puma 4.3.6 → 5.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +60 -12
  3. data/LICENSE +23 -20
  4. data/README.md +17 -11
  5. data/docs/deployment.md +3 -1
  6. data/docs/fork_worker.md +31 -0
  7. data/docs/jungle/README.md +13 -0
  8. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  9. data/{tools → docs}/jungle/rc.d/puma +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  11. data/{tools → docs}/jungle/upstart/README.md +0 -0
  12. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  13. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  14. data/docs/signals.md +1 -0
  15. data/docs/systemd.md +1 -63
  16. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  17. data/ext/puma_http11/extconf.rb +4 -3
  18. data/ext/puma_http11/http11_parser.c +1 -3
  19. data/ext/puma_http11/http11_parser.rl +1 -3
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/puma_http11.c +2 -38
  22. data/lib/puma.rb +4 -0
  23. data/lib/puma/app/status.rb +16 -5
  24. data/lib/puma/binder.rb +62 -60
  25. data/lib/puma/cli.rb +7 -15
  26. data/lib/puma/client.rb +30 -15
  27. data/lib/puma/cluster.rb +179 -74
  28. data/lib/puma/configuration.rb +30 -42
  29. data/lib/puma/const.rb +2 -3
  30. data/lib/puma/control_cli.rb +27 -17
  31. data/lib/puma/detect.rb +8 -0
  32. data/lib/puma/dsl.rb +70 -34
  33. data/lib/puma/io_buffer.rb +9 -2
  34. data/lib/puma/jruby_restart.rb +0 -58
  35. data/lib/puma/launcher.rb +41 -29
  36. data/lib/puma/minissl.rb +13 -8
  37. data/lib/puma/null_io.rb +1 -1
  38. data/lib/puma/plugin.rb +1 -10
  39. data/lib/puma/rack/builder.rb +0 -4
  40. data/lib/puma/reactor.rb +6 -1
  41. data/lib/puma/runner.rb +5 -34
  42. data/lib/puma/server.rb +62 -177
  43. data/lib/puma/single.rb +7 -64
  44. data/lib/puma/state_file.rb +5 -2
  45. data/lib/puma/thread_pool.rb +85 -47
  46. data/lib/rack/handler/puma.rb +1 -3
  47. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  48. metadata +19 -23
  49. data/docs/tcp_mode.md +0 -96
  50. data/ext/puma_http11/io_buffer.c +0 -155
  51. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  52. data/lib/puma/tcp_logger.rb +0 -41
  53. data/tools/jungle/README.md +0 -19
  54. data/tools/jungle/init.d/README.md +0 -61
  55. data/tools/jungle/init.d/puma +0 -421
  56. data/tools/jungle/init.d/run-puma +0 -18
@@ -30,8 +30,8 @@ public class Http11 extends RubyObject {
30
30
  public final static String MAX_REQUEST_URI_LENGTH_ERR = "HTTP element REQUEST_URI is longer than the 12288 allowed length.";
31
31
  public final static int MAX_FRAGMENT_LENGTH = 1024;
32
32
  public final static String MAX_FRAGMENT_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 1024 allowed length.";
33
- public final static int MAX_REQUEST_PATH_LENGTH = 2048;
34
- public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 2048 allowed length.";
33
+ public final static int MAX_REQUEST_PATH_LENGTH = 8192;
34
+ public final static String MAX_REQUEST_PATH_LENGTH_ERR = "HTTP element REQUEST_PATH is longer than the 8192 allowed length.";
35
35
  public final static int MAX_QUERY_STRING_LENGTH = 1024 * 10;
36
36
  public final static String MAX_QUERY_STRING_LENGTH_ERR = "HTTP element QUERY_STRING is longer than the 10240 allowed length.";
37
37
  public final static int MAX_HEADER_LENGTH = 1024 * (80 + 32);
@@ -197,7 +197,7 @@ public class Http11 extends RubyObject {
197
197
  validateMaxLength(runtime, parser.nread,MAX_HEADER_LENGTH, MAX_HEADER_LENGTH_ERR);
198
198
 
199
199
  if(hp.has_error()) {
200
- throw newHTTPParserError(runtime, "Invalid HTTP format, parsing fails.");
200
+ throw newHTTPParserError(runtime, "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
201
201
  } else {
202
202
  return runtime.newFixnum(parser.nread);
203
203
  }
@@ -54,7 +54,7 @@ DEF_MAX_LENGTH(FIELD_NAME, 256);
54
54
  DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
55
55
  DEF_MAX_LENGTH(REQUEST_URI, 1024 * 12);
56
56
  DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
57
- DEF_MAX_LENGTH(REQUEST_PATH, 2048);
57
+ DEF_MAX_LENGTH(REQUEST_PATH, 8196);
58
58
  DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10));
59
59
  DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
60
60
 
@@ -112,21 +112,6 @@ static struct common_field common_http_fields[] = {
112
112
  # undef f
113
113
  };
114
114
 
115
- /*
116
- * qsort(3) and bsearch(3) improve average performance slightly, but may
117
- * not be worth it for lack of portability to certain platforms...
118
- */
119
- #if defined(HAVE_QSORT_BSEARCH)
120
- /* sort by length, then by name if there's a tie */
121
- static int common_field_cmp(const void *a, const void *b)
122
- {
123
- struct common_field *cfa = (struct common_field *)a;
124
- struct common_field *cfb = (struct common_field *)b;
125
- signed long diff = cfa->len - cfb->len;
126
- return diff ? diff : memcmp(cfa->name, cfb->name, cfa->len);
127
- }
128
- #endif /* HAVE_QSORT_BSEARCH */
129
-
130
115
  static void init_common_fields(void)
131
116
  {
132
117
  unsigned i;
@@ -143,28 +128,10 @@ static void init_common_fields(void)
143
128
  }
144
129
  rb_global_variable(&cf->value);
145
130
  }
146
-
147
- #if defined(HAVE_QSORT_BSEARCH)
148
- qsort(common_http_fields,
149
- ARRAY_SIZE(common_http_fields),
150
- sizeof(struct common_field),
151
- common_field_cmp);
152
- #endif /* HAVE_QSORT_BSEARCH */
153
131
  }
154
132
 
155
133
  static VALUE find_common_field_value(const char *field, size_t flen)
156
134
  {
157
- #if defined(HAVE_QSORT_BSEARCH)
158
- struct common_field key;
159
- struct common_field *found;
160
- key.name = field;
161
- key.len = (signed long)flen;
162
- found = (struct common_field *)bsearch(&key, common_http_fields,
163
- ARRAY_SIZE(common_http_fields),
164
- sizeof(struct common_field),
165
- common_field_cmp);
166
- return found ? found->value : Qnil;
167
- #else /* !HAVE_QSORT_BSEARCH */
168
135
  unsigned i;
169
136
  struct common_field *cf = common_http_fields;
170
137
  for(i = 0; i < ARRAY_SIZE(common_http_fields); i++, cf++) {
@@ -172,7 +139,6 @@ static VALUE find_common_field_value(const char *field, size_t flen)
172
139
  return cf->value;
173
140
  }
174
141
  return Qnil;
175
- #endif /* !HAVE_QSORT_BSEARCH */
176
142
  }
177
143
 
178
144
  void http_field(puma_parser* hp, const char *field, size_t flen,
@@ -401,7 +367,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
401
367
  VALIDATE_MAX_LENGTH(puma_parser_nread(http), HEADER);
402
368
 
403
369
  if(puma_parser_has_error(http)) {
404
- rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails.");
370
+ rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
405
371
  } else {
406
372
  return INT2FIX(puma_parser_nread(http));
407
373
  }
@@ -468,7 +434,6 @@ VALUE HttpParser_body(VALUE self) {
468
434
  return http->body;
469
435
  }
470
436
 
471
- void Init_io_buffer(VALUE puma);
472
437
  void Init_mini_ssl(VALUE mod);
473
438
 
474
439
  void Init_puma_http11()
@@ -498,6 +463,5 @@ void Init_puma_http11()
498
463
  rb_define_method(cHttpParser, "body", HttpParser_body, 0);
499
464
  init_common_fields();
500
465
 
501
- Init_io_buffer(mPuma);
502
466
  Init_mini_ssl(mPuma);
503
467
  }
@@ -20,6 +20,10 @@ module Puma
20
20
  end
21
21
 
22
22
  def self.stats
23
+ @get_stats.stats.to_json
24
+ end
25
+
26
+ def self.stats_hash
23
27
  @get_stats.stats
24
28
  end
25
29
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Puma
4
6
  module App
5
7
  # Check out {#call}'s source code to see what actions this web application
@@ -17,10 +19,6 @@ module Puma
17
19
  return rack_response(403, 'Invalid auth token', 'text/plain')
18
20
  end
19
21
 
20
- if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
21
- require 'json'
22
- end
23
-
24
22
  case env['PATH_INFO']
25
23
  when /\/stop$/
26
24
  @cli.stop
@@ -56,7 +54,20 @@ module Puma
56
54
  rack_response(200, GC.stat.to_json)
57
55
 
58
56
  when /\/stats$/
59
- rack_response(200, @cli.stats)
57
+ rack_response(200, @cli.stats.to_json)
58
+
59
+ when /\/thread-backtraces$/
60
+ backtraces = []
61
+ @cli.thread_status do |name, backtrace|
62
+ backtraces << { name: name, backtrace: backtrace }
63
+ end
64
+
65
+ rack_response(200, backtraces.to_json)
66
+
67
+ when /\/refork$/
68
+ Process.kill "SIGURG", $$
69
+ rack_response(200, OK_STATUS)
70
+
60
71
  else
61
72
  rack_response 404, "Unsupported action", 'text/plain'
62
73
  end
@@ -11,7 +11,7 @@ module Puma
11
11
  class Binder
12
12
  include Puma::Const
13
13
 
14
- RACK_VERSION = [1,3].freeze
14
+ RACK_VERSION = [1,6].freeze
15
15
 
16
16
  def initialize(events)
17
17
  @events = events
@@ -43,7 +43,8 @@ module Puma
43
43
  @ios = []
44
44
  end
45
45
 
46
- attr_reader :ios
46
+ attr_reader :ios, :listeners, :unix_paths, :proto_env, :envs, :activated_sockets, :inherited_fds
47
+ attr_writer :ios, :listeners
47
48
 
48
49
  def env(sock)
49
50
  @envs.fetch(sock, @proto_env)
@@ -53,40 +54,39 @@ module Puma
53
54
  @ios.each { |i| i.close }
54
55
  end
55
56
 
56
- def import_from_env
57
- remove = []
58
-
59
- ENV.each do |k,v|
60
- if k =~ /PUMA_INHERIT_\d+/
61
- fd, url = v.split(":", 2)
62
- @inherited_fds[url] = fd.to_i
63
- remove << k
64
- elsif k == 'LISTEN_FDS' && ENV['LISTEN_PID'].to_i == $$
65
- v.to_i.times do |num|
66
- fd = num + 3
67
- sock = TCPServer.for_fd(fd)
68
- begin
69
- key = [ :unix, Socket.unpack_sockaddr_un(sock.getsockname) ]
70
- rescue ArgumentError
71
- port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
72
- if addr =~ /\:/
73
- addr = "[#{addr}]"
74
- end
75
- key = [ :tcp, addr, port ]
76
- end
77
- @activated_sockets[key] = sock
78
- @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
79
- end
80
- remove << k << 'LISTEN_PID'
81
- end
82
- end
57
+ def connected_ports
58
+ ios.map { |io| io.addr[1] }.uniq
59
+ end
60
+
61
+ def create_inherited_fds(env_hash)
62
+ env_hash.select {|k,v| k =~ /PUMA_INHERIT_\d+/}.each do |_k, v|
63
+ fd, url = v.split(":", 2)
64
+ @inherited_fds[url] = fd.to_i
65
+ end.keys # pass keys back for removal
66
+ end
83
67
 
84
- remove.each do |k|
85
- ENV.delete k
68
+ # systemd socket activation.
69
+ # LISTEN_FDS = number of listening sockets. e.g. 2 means accept on 2 sockets w/descriptors 3 and 4.
70
+ # LISTEN_PID = PID of the service process, aka us
71
+ # see https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html
72
+ def create_activated_fds(env_hash)
73
+ return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
74
+ env_hash['LISTEN_FDS'].to_i.times do |index|
75
+ sock = TCPServer.for_fd(socket_activation_fd(index))
76
+ key = begin # Try to parse as a path
77
+ [:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
78
+ rescue ArgumentError # Try to parse as a port/ip
79
+ port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
80
+ addr = "[#{addr}]" if addr =~ /\:/
81
+ [:tcp, addr, port]
82
+ end
83
+ @activated_sockets[key] = sock
84
+ @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
86
85
  end
86
+ ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
87
87
  end
88
88
 
89
- def parse(binds, logger)
89
+ def parse(binds, logger, log_msg = 'Listening')
90
90
  binds.each do |str|
91
91
  uri = URI.parse str
92
92
  case uri.scheme
@@ -113,7 +113,7 @@ module Puma
113
113
  i.local_address.ip_unpack.join(':')
114
114
  end
115
115
 
116
- logger.log "* Listening on tcp://#{addr}"
116
+ logger.log "* #{log_msg} on tcp://#{addr}"
117
117
  end
118
118
  end
119
119
 
@@ -149,7 +149,7 @@ module Puma
149
149
  end
150
150
 
151
151
  io = add_unix_listener path, umask, mode, backlog
152
- logger.log "* Listening on #{str}"
152
+ logger.log "* #{log_msg} on #{str}"
153
153
  end
154
154
 
155
155
  @listeners << [str, io]
@@ -204,12 +204,6 @@ module Puma
204
204
  end
205
205
  end
206
206
 
207
- def loopback_addresses
208
- Socket.ip_address_list.select do |addrinfo|
209
- addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
210
- end.map { |addrinfo| addrinfo.ip_address }.uniq
211
- end
212
-
213
207
  # Tell the server to listen on host +host+, port +port+.
214
208
  # If +optimize_for_latency+ is true (the default) then clients connecting
215
209
  # will be optimized for latency over throughput.
@@ -226,20 +220,17 @@ module Puma
226
220
  end
227
221
 
228
222
  host = host[1..-2] if host and host[0..0] == '['
229
- s = TCPServer.new(host, port)
223
+ tcp_server = TCPServer.new(host, port)
230
224
  if optimize_for_latency
231
- s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
225
+ tcp_server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
232
226
  end
233
- s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
234
- s.listen backlog
235
- @connected_port = s.addr[1]
227
+ tcp_server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
228
+ tcp_server.listen backlog
236
229
 
237
- @ios << s
238
- s
230
+ @ios << tcp_server
231
+ tcp_server
239
232
  end
240
233
 
241
- attr_reader :connected_port
242
-
243
234
  def inherit_tcp_listener(host, port, fd)
244
235
  if fd.kind_of? TCPServer
245
236
  s = fd
@@ -360,26 +351,37 @@ module Puma
360
351
  end
361
352
 
362
353
  def close_listeners
363
- @listeners.each do |l, io|
364
- io.close
354
+ listeners.each do |l, io|
355
+ io.close unless io.closed? # Ruby 2.2 issue
365
356
  uri = URI.parse(l)
366
357
  next unless uri.scheme == 'unix'
367
358
  unix_path = "#{uri.host}#{uri.path}"
368
- File.unlink unix_path if @unix_paths.include? unix_path
359
+ File.unlink unix_path if unix_paths.include? unix_path
369
360
  end
370
361
  end
371
362
 
372
- def close_unix_paths
373
- @unix_paths.each { |up| File.unlink(up) if File.exist? up }
363
+ def redirects_for_restart
364
+ redirects = listeners.map { |a| [a[1].to_i, a[1].to_i] }.to_h
365
+ redirects[:close_others] = true
366
+ redirects
374
367
  end
375
368
 
376
- def redirects_for_restart
377
- redirects = {:close_others => true}
378
- @listeners.each_with_index do |(l, io), i|
379
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
380
- redirects[io.to_i] = io.to_i
369
+ def redirects_for_restart_env
370
+ listeners.each_with_object({}).with_index do |(listen, memo), i|
371
+ memo["PUMA_INHERIT_#{i}"] = "#{listen[1].to_i}:#{listen[0]}"
381
372
  end
382
- redirects
373
+ end
374
+
375
+ private
376
+
377
+ def loopback_addresses
378
+ Socket.ip_address_list.select do |addrinfo|
379
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
380
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
381
+ end
382
+
383
+ def socket_activation_fd(int)
384
+ int + 3 # 3 is the magic number you add to follow the SA protocol
383
385
  end
384
386
  end
385
387
  end
@@ -80,7 +80,7 @@ module Puma
80
80
  @launcher.run
81
81
  end
82
82
 
83
- private
83
+ private
84
84
  def unsupported(str)
85
85
  @events.error(str)
86
86
  raise UnsupportedOption
@@ -112,21 +112,11 @@ module Puma
112
112
  configure_control_url(arg)
113
113
  end
114
114
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
- configure_control_url(arg)
118
- end
119
-
120
115
  o.on "--control-token TOKEN",
121
116
  "The token to use as authentication for the control server" do |arg|
122
117
  @control_options[:auth_token] = arg
123
118
  end
124
119
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
120
  o.on "--debug", "Log lowlevel debugging information" do
131
121
  user_config.debug
132
122
  end
@@ -140,6 +130,12 @@ module Puma
140
130
  user_config.environment arg
141
131
  end
142
132
 
133
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
134
+ "Fork new workers from existing worker. Cluster mode only",
135
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
136
+ user_config.fork_worker(*args.compact)
137
+ end
138
+
143
139
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
140
  $LOAD_PATH.unshift(*arg.split(':'))
145
141
  end
@@ -192,10 +188,6 @@ module Puma
192
188
  end
193
189
  end
194
190
 
195
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
196
- user_config.tcp_mode!
197
- end
198
-
199
191
  o.on "--early-hints", "Enable early hints support" do
200
192
  user_config.early_hints
201
193
  end
@@ -153,7 +153,7 @@ module Puma
153
153
 
154
154
  begin
155
155
  data = @io.read_nonblock(CHUNK_SIZE)
156
- rescue Errno::EAGAIN
156
+ rescue IO::WaitReadable
157
157
  return false
158
158
  rescue SystemCallError, IOError, EOFError
159
159
  raise ConnectionError, "Connection error detected during read"
@@ -238,12 +238,23 @@ module Puma
238
238
  return false unless IO.select([@to_io], nil, nil, 0)
239
239
  try_to_finish
240
240
  end
241
+
242
+ # For documentation, see https://github.com/puma/puma/issues/1754
243
+ send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
241
244
  end # IS_JRUBY
242
245
 
243
- def finish
246
+ def finish(timeout)
244
247
  return true if @ready
245
248
  until try_to_finish
246
- IO.select([@to_io], nil, nil)
249
+ can_read = begin
250
+ IO.select([@to_io], nil, nil, timeout)
251
+ rescue ThreadPool::ForceShutdown
252
+ nil
253
+ end
254
+ unless can_read
255
+ write_error(408) if in_data_phase
256
+ raise ConnectionError
257
+ end
247
258
  end
248
259
  true
249
260
  end
@@ -259,7 +270,7 @@ module Puma
259
270
  return @peerip if @peerip
260
271
 
261
272
  if @remote_addr_header
262
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
273
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
263
274
  @peerip = hdr
264
275
  return hdr
265
276
  end
@@ -267,6 +278,18 @@ module Puma
267
278
  @peerip ||= @io.peeraddr.last
268
279
  end
269
280
 
281
+ # Returns true if the persistent connection can be closed immediately
282
+ # without waiting for the configured idle/shutdown timeout.
283
+ def can_close?
284
+ # Allow connection to close if it's received at least one full request
285
+ # and hasn't received any data for a future request.
286
+ #
287
+ # From RFC 2616 section 8.1.4:
288
+ # Servers SHOULD always respond to at least one request per connection,
289
+ # if at all possible.
290
+ @requests_served > 0 && @parsed_bytes == 0
291
+ end
292
+
270
293
  private
271
294
 
272
295
  def setup_body
@@ -285,16 +308,8 @@ module Puma
285
308
 
286
309
  te = @env[TRANSFER_ENCODING2]
287
310
 
288
- if te
289
- if te.include?(",")
290
- te.split(",").each do |part|
291
- if CHUNKED.casecmp(part.strip) == 0
292
- return setup_chunked_body(body)
293
- end
294
- end
295
- elsif CHUNKED.casecmp(te) == 0
296
- return setup_chunked_body(body)
297
- end
311
+ if te && CHUNKED.casecmp(te) == 0
312
+ return setup_chunked_body(body)
298
313
  end
299
314
 
300
315
  @chunked_body = false
@@ -351,7 +366,7 @@ module Puma
351
366
 
352
367
  begin
353
368
  chunk = @io.read_nonblock(want)
354
- rescue Errno::EAGAIN
369
+ rescue IO::WaitReadable
355
370
  return false
356
371
  rescue SystemCallError, IOError
357
372
  raise ConnectionError, "Connection error detected during read"