diff --git a/ext/puma_http11/puma_http11.c b/ext/puma_http11/puma_http11.c index 82a4705c..caf97633 100644 --- a/ext/puma_http11/puma_http11.c +++ b/ext/puma_http11/puma_http11.c @@ -57,7 +57,7 @@ DEF_MAX_LENGTH(QUERY_STRING, (1024 * 10)); DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32))); struct common_field { - const signed long len; + const size_t len; const char *name; int raw; VALUE value; @@ -127,7 +127,7 @@ static int common_field_cmp(const void *a, const void *b) static void init_common_fields(void) { - int i; + unsigned i; struct common_field *cf = common_http_fields; char tmp[256]; /* MAX_FIELD_NAME_LENGTH */ memcpy(tmp, HTTP_PREFIX, HTTP_PREFIX_LEN); @@ -163,7 +163,7 @@ static VALUE find_common_field_value(const char *field, size_t flen) common_field_cmp); return found ? found->value : Qnil; #else /* !HAVE_QSORT_BSEARCH */ - int i; + unsigned i; struct common_field *cf = common_http_fields; for(i = 0; i < ARRAY_SIZE(common_http_fields); i++, cf++) { if (cf->len == flen && !memcmp(cf->name, field, flen)) @@ -460,6 +460,7 @@ void Init_puma_http11() { VALUE mPuma = rb_define_module("Puma"); + VALUE cHttpParser = rb_define_class_under(mPuma, "HttpParser", rb_cObject); DEF_GLOBAL(request_method, "REQUEST_METHOD"); DEF_GLOBAL(request_uri, "REQUEST_URI"); @@ -471,7 +472,6 @@ void Init_puma_http11() eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eIOError); rb_global_variable(&eHttpParserError); - VALUE cHttpParser = rb_define_class_under(mPuma, "HttpParser", rb_cObject); rb_define_alloc_func(cHttpParser, HttpParser_alloc); rb_define_method(cHttpParser, "initialize", HttpParser_init, 0); rb_define_method(cHttpParser, "reset", HttpParser_reset, 0); diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index 9a1b9bbc..eacb85cf 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -5,6 +5,7 @@ require 'puma/server' require 'puma/const' require 'puma/configuration' require 'puma/binder' +require 'puma/detect' require 'rack/commonlogger' require 'rack/utils' @@ -13,8 +14,6 @@ module Puma # Handles invoke a Puma::Server in a command line style. # class CLI - IS_JRUBY = defined?(JRUBY_VERSION) - # Create a new CLI object using +argv+ as the command line # arguments. # diff --git a/lib/puma/client.rb b/lib/puma/client.rb index 4e227ecc..916a3f6e 100644 --- a/lib/puma/client.rb +++ b/lib/puma/client.rb @@ -1,3 +1,20 @@ +class IO + # We need to use this for a jruby work around on both 1.8 and 1.9. + # So this either creates the constant (on 1.8), or harmlessly + # reopens it (on 1.9). + module WaitReadable + end +end + +require 'puma/detect' + +if Puma::IS_JRUBY + # We have to work around some OpenSSL buffer/io-readiness bugs + # so we pull it in regardless of if the user is binding + # to an SSL socket + require 'openssl' +end + module Puma class Client include Puma::Const @@ -17,10 +34,16 @@ module Puma @buffer = nil @timeout_at = nil + + @requests_served = 0 end attr_reader :env, :to_io, :body, :io, :timeout_at, :ready + def inspect + "#" + end + def set_timeout(val) @timeout_at = Time.now + val end @@ -67,6 +90,7 @@ module Puma unless cl @buffer = body.empty? ? nil : body @body = EmptyBody + @requests_served += 1 @ready = true return true end @@ -76,6 +100,7 @@ module Puma if remain <= 0 @body = StringIO.new(body) @buffer = nil + @requests_served += 1 @ready = true return true end @@ -121,11 +146,54 @@ module Puma false end - def eagerly_finish - return true if @ready - return false unless IO.select([@to_io], nil, nil, 0) - try_to_finish - end + if IS_JRUBY + def jruby_start_try_to_finish + return read_body unless @read_header + + begin + data = @io.sysread_nonblock(CHUNK_SIZE) + rescue OpenSSL::SSL::SSLError => e + return false if e.kind_of? IO::WaitReadable + raise e + end + + if @buffer + @buffer << data + else + @buffer = data + end + + @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes) + + if @parser.finished? + return setup_body + elsif @parsed_bytes >= MAX_HEADER + raise HttpParserError, + "HEADER is longer than allowed, aborting client early." + end + + false + end + + def eagerly_finish + return true if @ready + + if @io.kind_of? OpenSSL::SSL::SSLSocket + return true if jruby_start_try_to_finish + end + + return false unless IO.select([@to_io], nil, nil, 0) + try_to_finish + end + + else + + def eagerly_finish + return true if @ready + return false unless IO.select([@to_io], nil, nil, 0) + try_to_finish + end + end # IS_JRUBY def read_body # Read an odd sized chunk so we can read even sized ones @@ -144,6 +212,7 @@ module Puma unless chunk @body.close @buffer = nil + @requests_served += 1 @ready = true raise EOFError end @@ -153,6 +222,7 @@ module Puma if remain <= 0 @body.rewind @buffer = nil + @requests_served += 1 @ready = true return true end diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 6cfa684e..c11848b2 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -33,6 +33,10 @@ module Puma # session. PERSISTENT_TIMEOUT = 20 + # The default number of seconds to wait until we get the first data + # for the request + FIRST_DATA_TIMEOUT = 30 + DATE = "Date".freeze SCRIPT_NAME = "SCRIPT_NAME".freeze diff --git a/lib/puma/detect.rb b/lib/puma/detect.rb new file mode 100644 index 00000000..8fa96499 --- /dev/null +++ b/lib/puma/detect.rb @@ -0,0 +1,4 @@ +module Puma + IS_JRUBY = defined?(JRUBY_VERSION) +end + diff --git a/lib/puma/reactor.rb b/lib/puma/reactor.rb index 75087d56..ea44bd94 100644 --- a/lib/puma/reactor.rb +++ b/lib/puma/reactor.rb @@ -24,9 +24,13 @@ module Puma reads.each do |c| if c == @ready @mutex.synchronize do - @ready.read(1) # drain - sockets += @input - @input.clear + case @ready.read(1) + when "*" + sockets += @input + @input.clear + when "!" + return + end end else # We have to be sure to remove it from the timeout @@ -41,6 +45,7 @@ module Puma @app_pool << c sockets.delete c end + # The client doesn't know HTTP well rescue HttpParserError => e c.close @@ -48,7 +53,7 @@ module Puma @events.parse_error @server, c.env, e - rescue EOFError + rescue IOError => e c.close sockets.delete c end @@ -73,7 +78,15 @@ module Puma end def run_in_thread - @thread = Thread.new { run } + @thread = Thread.new { + begin + run + rescue Exception => e + puts "MAJOR ERROR DETECTED" + p e + puts e.backtrace + end + } end def calculate_sleep @@ -93,7 +106,7 @@ module Puma def add(c) @mutex.synchronize do @input << c - @trigger << "!" + @trigger << "*" if c.timeout_at @timeouts << c @@ -103,5 +116,9 @@ module Puma end end end + + def shutdown + @trigger << "!" + end end end diff --git a/lib/puma/server.rb b/lib/puma/server.rb index 41c3d1d5..9d41314f 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -60,6 +60,7 @@ module Puma @persistent_check, @persistent_wakeup = IO.pipe @binder = Binder.new(events) + @first_data_timeout = FIRST_DATA_TIMEOUT ENV['RACK_ENV'] ||= "development" end @@ -112,7 +113,6 @@ module Puma @status = :run @thread_pool = ThreadPool.new(@min_threads, @max_threads) do |client| - process_now = false begin @@ -120,12 +120,13 @@ module Puma rescue HttpParserError => e client.close @events.parse_error self, client.env, e - rescue EOFError + rescue IOError client.close else if process_now process_client client else + client.set_timeout @first_data_timeout @reactor.add client end end @@ -163,7 +164,7 @@ module Puma begin if io = sock.accept_nonblock c = Client.new io, @binder.env(sock) - @thread_pool << c + pool << c end rescue SystemCallError => e end @@ -177,6 +178,7 @@ module Puma end end + @reactor.shutdown graceful_shutdown if @status == :stop ensure unless @status == :restart @@ -233,7 +235,7 @@ module Puma end # The client disconnected while we were reading data - rescue EOFError, SystemCallError + rescue IOError, SystemCallError => e # Swallow them. The ensure tries to close +client+ down # The client doesn't know HTTP well