diff options
-rw-r--r-- | Manifest | 12 | ||||
-rw-r--r-- | README | 2 | ||||
-rwxr-xr-x | bin/unicorn_rails | 42 | ||||
-rw-r--r-- | ext/unicorn/http11/http11.c | 123 | ||||
-rw-r--r-- | ext/unicorn/http11/http11_parser.c | 47 | ||||
-rw-r--r-- | ext/unicorn/http11/http11_parser.h | 4 | ||||
-rw-r--r-- | ext/unicorn/http11/http11_parser.rl | 3 | ||||
-rw-r--r-- | lib/unicorn.rb | 2 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails.rb | 23 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails/static.rb | 58 | ||||
-rw-r--r-- | lib/unicorn/cgi_wrapper.rb | 139 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 34 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 118 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 6 | ||||
-rw-r--r-- | lib/unicorn/socket.rb | 17 | ||||
-rw-r--r-- | test/benchmark/README | 55 | ||||
-rw-r--r-- | test/benchmark/big_request.rb | 35 | ||||
-rw-r--r-- | test/benchmark/dd.ru | 18 | ||||
-rw-r--r-- | test/benchmark/previous.rb | 11 | ||||
-rw-r--r-- | test/benchmark/request.rb | 47 | ||||
-rw-r--r-- | test/benchmark/response.rb | 29 | ||||
-rw-r--r-- | test/benchmark/simple.rb | 11 | ||||
-rw-r--r-- | test/benchmark/utils.rb | 82 | ||||
-rw-r--r-- | test/unit/test_http_parser.rb | 113 | ||||
-rw-r--r-- | test/unit/test_socket_helper.rb | 159 |
25 files changed, 820 insertions, 370 deletions
@@ -21,6 +21,9 @@ ext/unicorn/http11/http11_parser.rl ext/unicorn/http11/http11_parser_common.rl lib/unicorn.rb lib/unicorn/app/exec_cgi.rb +lib/unicorn/app/old_rails.rb +lib/unicorn/app/old_rails/static.rb +lib/unicorn/cgi_wrapper.rb lib/unicorn/configurator.rb lib/unicorn/const.rb lib/unicorn/http_request.rb @@ -30,9 +33,11 @@ lib/unicorn/socket.rb lib/unicorn/util.rb setup.rb test/aggregate.rb -test/benchmark/previous.rb -test/benchmark/simple.rb -test/benchmark/utils.rb +test/benchmark/README +test/benchmark/big_request.rb +test/benchmark/dd.ru +test/benchmark/request.rb +test/benchmark/response.rb test/exec/README test/exec/test_exec.rb test/test_helper.rb @@ -42,4 +47,5 @@ test/unit/test_http_parser.rb test/unit/test_request.rb test/unit/test_response.rb test/unit/test_server.rb +test/unit/test_socket_helper.rb test/unit/test_upload.rb @@ -90,7 +90,7 @@ of your application or libraries. However, your Rack application may use threads internally (and should even be able to continue running threads after the request is complete). -=== Rack-enabled versions of Rails (v2.3.2+) +=== for Rails applications (should work for all 1.2 or later versions) In RAILS_ROOT, run: diff --git a/bin/unicorn_rails b/bin/unicorn_rails index 177c109..fae6f4b 100755 --- a/bin/unicorn_rails +++ b/bin/unicorn_rails @@ -7,6 +7,7 @@ rails_pid = File.join(Unicorn::HttpServer::DEFAULT_START_CTX[:cwd], "/tmp/pids/unicorn.pid") cmd = File.basename($0) daemonize = false +static = true listeners = [] options = { :listeners => listeners } host, port = Unicorn::Const::DEFAULT_HOST, 3000 @@ -123,7 +124,22 @@ rails_loader = lambda do || when nil lambda do || require 'config/environment' - ActionController::Dispatcher.new + + # it seems Rails >=2.2 support Rack, but only >=2.3 requires it + old_rails = case ::Rails::VERSION::MAJOR + when 0, 1 then true + when 2 then Rails::VERSION::MINOR < 3 ? true : false + else + false + end + + if old_rails + require 'rack' + require 'unicorn/app/old_rails' + Unicorn::App::OldRails.new + else + ActionController::Dispatcher.new + end end when /\.ru$/ raw = File.open(config, "rb") { |fp| fp.sysread(fp.stat.size) } @@ -147,12 +163,26 @@ app = lambda do || require 'active_support' require 'action_controller' ActionController::Base.relative_url_root = map_path if map_path + map_path ||= '/' + inner_app = inner_app.call Rack::Builder.new do - use Rails::Rack::LogTailer unless daemonize - use Rails::Rack::Debugger if $DEBUG - map(map_path || '/') do - use Rails::Rack::Static - run inner_app.call + if inner_app.class.to_s == "Unicorn::App::OldRails" + $stderr.puts "LogTailer not available for Rails < 2.3" unless daemonize + $stderr.puts "Debugger not available" if $DEBUG + map(map_path) do + if static + require 'unicorn/app/old_rails/static' + use Unicorn::App::OldRails::Static + end + run inner_app + end + else + use Rails::Rack::LogTailer unless daemonize + use Rails::Rack::Debugger if $DEBUG + map(map_path) do + use Rails::Rack::Static if static + run inner_app + end end end.to_app end diff --git a/ext/unicorn/http11/http11.c b/ext/unicorn/http11/http11.c index 0b96099..f62dce7 100644 --- a/ext/unicorn/http11/http11.c +++ b/ext/unicorn/http11/http11.c @@ -1,4 +1,5 @@ /** + * Copyright (c) 2009 Eric Wong (all bugs are Eric's fault) * Copyright (c) 2005 Zed A. Shaw * You can redistribute it and/or modify it under the same terms as Ruby. */ @@ -367,113 +368,37 @@ static VALUE HttpParser_reset(VALUE self) /** * call-seq: - * parser.finish -> true/false + * parser.execute(req_hash, data) -> true/false * - * Finishes a parser early which could put in a "good" or bad state. - * You should call reset after finish it or bad things will happen. - */ -static VALUE HttpParser_finish(VALUE self) -{ - http_parser *http = NULL; - DATA_GET(self, http_parser, http); - http_parser_finish(http); - - return http_parser_is_finished(http) ? Qtrue : Qfalse; -} - - -/** - * call-seq: - * parser.execute(req_hash, data, start) -> Integer - * - * Takes a Hash and a String of data, parses the String of data filling in the Hash - * returning an Integer to indicate how much of the data has been read. No matter - * what the return value, you should call HttpParser#finished? and HttpParser#error? - * to figure out if it's done parsing or there was an error. + * Takes a Hash and a String of data, parses the String of data filling + * in the Hash returning a boolean to indicate whether or not parsing + * is finished. * - * This function now throws an exception when there is a parsing error. This makes - * the logic for working with the parser much easier. You can still test for an - * error, but now you need to wrap the parser with an exception handling block. - * - * The third argument allows for parsing a partial request and then continuing - * the parsing from that position. It needs all of the original data as well - * so you have to append to the data buffer as you read. + * This function now throws an exception when there is a parsing error. + * This makes the logic for working with the parser much easier. You + * will need to wrap the parser with an exception handling block. */ -static VALUE HttpParser_execute(VALUE self, VALUE req_hash, - VALUE data, VALUE start) -{ - http_parser *http = NULL; - int from = 0; - char *dptr = NULL; - long dlen = 0; - - DATA_GET(self, http_parser, http); - - from = FIX2INT(start); - dptr = RSTRING_PTR(data); - dlen = RSTRING_LEN(data); - - if(from >= dlen) { - rb_raise(eHttpParserError, "Requested start is after data buffer end."); - } else { - http->data = (void *)req_hash; - http_parser_execute(http, dptr, dlen, from); - - VALIDATE_MAX_LENGTH(http_parser_nread(http), HEADER); - - if(http_parser_has_error(http)) { - rb_raise(eHttpParserError, "Invalid HTTP format, parsing fails."); - } else { - return INT2FIX(http_parser_nread(http)); - } - } -} - - -/** - * call-seq: - * parser.error? -> true/false - * - * Tells you whether the parser is in an error state. - */ -static VALUE HttpParser_has_error(VALUE self) +static VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data) { - http_parser *http = NULL; - DATA_GET(self, http_parser, http); + http_parser *http; + char *dptr = RSTRING_PTR(data); + long dlen = RSTRING_LEN(data); - return http_parser_has_error(http) ? Qtrue : Qfalse; -} - - -/** - * call-seq: - * parser.finished? -> true/false - * - * Tells you whether the parser is finished or not and in a good state. - */ -static VALUE HttpParser_is_finished(VALUE self) -{ - http_parser *http = NULL; DATA_GET(self, http_parser, http); - return http_parser_is_finished(http) ? Qtrue : Qfalse; -} + if (http->nread < dlen) { + http->data = (void *)req_hash; + http_parser_execute(http, dptr, dlen); + VALIDATE_MAX_LENGTH(http->nread, HEADER); -/** - * call-seq: - * parser.nread -> Integer - * - * Returns the amount of data processed so far during this processing cycle. It is - * set to 0 on initialize or reset calls and is incremented each time execute is called. - */ -static VALUE HttpParser_nread(VALUE self) -{ - http_parser *http = NULL; - DATA_GET(self, http_parser, http); + if (!http_parser_has_error(http)) + return http_parser_is_finished(http) ? Qtrue : Qfalse; - return INT2FIX(http->nread); + rb_raise(eHttpParserError, "Invalid HTTP format, parsing fails."); + } + rb_raise(eHttpParserError, "Requested start is after data buffer end."); } void Init_http11() @@ -504,10 +429,6 @@ void Init_http11() rb_define_alloc_func(cHttpParser, HttpParser_alloc); rb_define_method(cHttpParser, "initialize", HttpParser_init,0); rb_define_method(cHttpParser, "reset", HttpParser_reset,0); - rb_define_method(cHttpParser, "finish", HttpParser_finish,0); - rb_define_method(cHttpParser, "execute", HttpParser_execute,3); - rb_define_method(cHttpParser, "error?", HttpParser_has_error,0); - rb_define_method(cHttpParser, "finished?", HttpParser_is_finished,0); - rb_define_method(cHttpParser, "nread", HttpParser_nread,0); + rb_define_method(cHttpParser, "execute", HttpParser_execute,2); init_common_fields(); } diff --git a/ext/unicorn/http11/http11_parser.c b/ext/unicorn/http11/http11_parser.c index d33eed0..b6d55c8 100644 --- a/ext/unicorn/http11/http11_parser.c +++ b/ext/unicorn/http11/http11_parser.c @@ -63,9 +63,10 @@ int http_parser_init(http_parser *parser) { /** exec **/ -size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len, size_t off) { +size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len) { const char *p, *pe; int cs = parser->cs; + size_t off = parser->nread; assert(off <= len && "offset past end of buffer"); @@ -76,7 +77,7 @@ size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len, assert(pe - p == len - off && "pointers aren't same distance"); -#line 80 "http11_parser.c" +#line 81 "http11_parser.c" { if ( p == pe ) goto _test_eof; @@ -107,7 +108,7 @@ st2: if ( ++p == pe ) goto _test_eof2; case 2: -#line 111 "http11_parser.c" +#line 112 "http11_parser.c" switch( (*p) ) { case 32: goto tr2; case 36: goto st38; @@ -133,7 +134,7 @@ st3: if ( ++p == pe ) goto _test_eof3; case 3: -#line 137 "http11_parser.c" +#line 138 "http11_parser.c" switch( (*p) ) { case 42: goto tr4; case 43: goto tr5; @@ -157,7 +158,7 @@ st4: if ( ++p == pe ) goto _test_eof4; case 4: -#line 161 "http11_parser.c" +#line 162 "http11_parser.c" switch( (*p) ) { case 32: goto tr8; case 35: goto tr9; @@ -228,7 +229,7 @@ st5: if ( ++p == pe ) goto _test_eof5; case 5: -#line 232 "http11_parser.c" +#line 233 "http11_parser.c" if ( (*p) == 72 ) goto tr10; goto st0; @@ -240,7 +241,7 @@ st6: if ( ++p == pe ) goto _test_eof6; case 6: -#line 244 "http11_parser.c" +#line 245 "http11_parser.c" if ( (*p) == 84 ) goto st7; goto st0; @@ -326,7 +327,7 @@ st14: if ( ++p == pe ) goto _test_eof14; case 14: -#line 330 "http11_parser.c" +#line 331 "http11_parser.c" if ( (*p) == 10 ) goto st15; goto st0; @@ -378,7 +379,7 @@ st57: if ( ++p == pe ) goto _test_eof57; case 57: -#line 382 "http11_parser.c" +#line 383 "http11_parser.c" goto st0; tr21: #line 37 "http11_parser.rl" @@ -394,7 +395,7 @@ st17: if ( ++p == pe ) goto _test_eof17; case 17: -#line 398 "http11_parser.c" +#line 399 "http11_parser.c" switch( (*p) ) { case 33: goto tr23; case 58: goto tr24; @@ -433,7 +434,7 @@ st18: if ( ++p == pe ) goto _test_eof18; case 18: -#line 437 "http11_parser.c" +#line 438 "http11_parser.c" switch( (*p) ) { case 13: goto tr26; case 32: goto tr27; @@ -447,7 +448,7 @@ st19: if ( ++p == pe ) goto _test_eof19; case 19: -#line 451 "http11_parser.c" +#line 452 "http11_parser.c" if ( (*p) == 13 ) goto tr29; goto st19; @@ -500,7 +501,7 @@ st20: if ( ++p == pe ) goto _test_eof20; case 20: -#line 504 "http11_parser.c" +#line 505 "http11_parser.c" switch( (*p) ) { case 32: goto tr31; case 35: goto st0; @@ -518,7 +519,7 @@ st21: if ( ++p == pe ) goto _test_eof21; case 21: -#line 522 "http11_parser.c" +#line 523 "http11_parser.c" switch( (*p) ) { case 32: goto tr34; case 35: goto st0; @@ -536,7 +537,7 @@ st22: if ( ++p == pe ) goto _test_eof22; case 22: -#line 540 "http11_parser.c" +#line 541 "http11_parser.c" if ( (*p) < 65 ) { if ( 48 <= (*p) && (*p) <= 57 ) goto st23; @@ -567,7 +568,7 @@ st24: if ( ++p == pe ) goto _test_eof24; case 24: -#line 571 "http11_parser.c" +#line 572 "http11_parser.c" switch( (*p) ) { case 43: goto st24; case 58: goto st25; @@ -592,7 +593,7 @@ st25: if ( ++p == pe ) goto _test_eof25; case 25: -#line 596 "http11_parser.c" +#line 597 "http11_parser.c" switch( (*p) ) { case 32: goto tr8; case 35: goto tr9; @@ -636,7 +637,7 @@ st28: if ( ++p == pe ) goto _test_eof28; case 28: -#line 640 "http11_parser.c" +#line 641 "http11_parser.c" switch( (*p) ) { case 32: goto tr42; case 35: goto tr43; @@ -685,7 +686,7 @@ st31: if ( ++p == pe ) goto _test_eof31; case 31: -#line 689 "http11_parser.c" +#line 690 "http11_parser.c" switch( (*p) ) { case 32: goto tr8; case 35: goto tr9; @@ -733,7 +734,7 @@ st34: if ( ++p == pe ) goto _test_eof34; case 34: -#line 737 "http11_parser.c" +#line 738 "http11_parser.c" switch( (*p) ) { case 32: goto tr53; case 35: goto tr54; @@ -751,7 +752,7 @@ st35: if ( ++p == pe ) goto _test_eof35; case 35: -#line 755 "http11_parser.c" +#line 756 "http11_parser.c" switch( (*p) ) { case 32: goto tr57; case 35: goto tr58; @@ -769,7 +770,7 @@ st36: if ( ++p == pe ) goto _test_eof36; case 36: -#line 773 "http11_parser.c" +#line 774 "http11_parser.c" if ( (*p) < 65 ) { if ( 48 <= (*p) && (*p) <= 57 ) goto st37; @@ -1184,7 +1185,7 @@ case 56: _test_eof: {} _out: {} } -#line 121 "http11_parser.rl" +#line 122 "http11_parser.rl" if (!http_parser_has_error(parser)) parser->cs = cs; diff --git a/ext/unicorn/http11/http11_parser.h b/ext/unicorn/http11/http11_parser.h index c96b3a0..6c332fe 100644 --- a/ext/unicorn/http11/http11_parser.h +++ b/ext/unicorn/http11/http11_parser.h @@ -36,10 +36,8 @@ typedef struct http_parser { int http_parser_init(http_parser *parser); int http_parser_finish(http_parser *parser); -size_t http_parser_execute(http_parser *parser, const char *data, size_t len, size_t off); +size_t http_parser_execute(http_parser *parser, const char *data, size_t len); int http_parser_has_error(http_parser *parser); int http_parser_is_finished(http_parser *parser); -#define http_parser_nread(parser) (parser)->nread - #endif diff --git a/ext/unicorn/http11/http11_parser.rl b/ext/unicorn/http11/http11_parser.rl index c3c4b1f..1fad2ca 100644 --- a/ext/unicorn/http11/http11_parser.rl +++ b/ext/unicorn/http11/http11_parser.rl @@ -105,9 +105,10 @@ int http_parser_init(http_parser *parser) { /** exec **/ -size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len, size_t off) { +size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len) { const char *p, *pe; int cs = parser->cs; + size_t off = parser->nread; assert(off <= len && "offset past end of buffer"); diff --git a/lib/unicorn.rb b/lib/unicorn.rb index eefbfc1..e36cb1e 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -135,7 +135,7 @@ module Unicorn def listen(address) return if String === address && listener_names.include?(address) - if io = bind_listen(address, @backlog) + if io = bind_listen(address, { :backlog => @backlog }) if Socket == io.class @io_purgatory << io io = server_cast(io) diff --git a/lib/unicorn/app/old_rails.rb b/lib/unicorn/app/old_rails.rb new file mode 100644 index 0000000..bb9577a --- /dev/null +++ b/lib/unicorn/app/old_rails.rb @@ -0,0 +1,23 @@ +# This code is based on the original Rails handler in Mongrel +# Copyright (c) 2005 Zed A. Shaw +# Copyright (c) 2009 Eric Wong +# You can redistribute it and/or modify it under the same terms as Ruby. +# Additional work donated by contributors. See CONTRIBUTORS for more info. +require 'unicorn/cgi_wrapper' +require 'dispatcher' + +module Unicorn; module App; end; end + +# Implements a handler that can run Rails. +class Unicorn::App::OldRails + + def call(env) + cgi = Unicorn::CGIWrapper.new(env) + Dispatcher.dispatch(cgi, + ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, + cgi.body) + cgi.out # finalize the response + cgi.rack_response + end + +end diff --git a/lib/unicorn/app/old_rails/static.rb b/lib/unicorn/app/old_rails/static.rb new file mode 100644 index 0000000..c9366d2 --- /dev/null +++ b/lib/unicorn/app/old_rails/static.rb @@ -0,0 +1,58 @@ +# This code is based on the original Rails handler in Mongrel +# Copyright (c) 2005 Zed A. Shaw +# Copyright (c) 2009 Eric Wong +# You can redistribute it and/or modify it under the same terms as Ruby. + +require 'rack/file' + +# Static file handler for Rails < 2.3. This handler is only provided +# as a convenience for developers. Performance-minded deployments should +# use nginx (or similar) for serving static files. +# +# This supports page caching directly and will try to resolve a +# request in the following order: +# +# * If the requested exact PATH_INFO exists as a file then serve it. +# * If it exists at PATH_INFO+rest_operator+".html" exists +# then serve that. +# +# This means that if you are using page caching it will actually work +# with Unicorn and you should see a decent speed boost (but not as +# fast as if you use a static server like nginx). +class Unicorn::App::OldRails::Static + FILE_METHODS = { 'GET' => true, 'HEAD' => true }.freeze + + def initialize(app) + @app = app + @root = "#{::RAILS_ROOT}/public" + @file_server = ::Rack::File.new(@root) + end + + def call(env) + # short circuit this ASAP if serving non-file methods + FILE_METHODS.include?(env[Unicorn::Const::REQUEST_METHOD]) or + return @app.call(env) + + # first try the path as-is + path_info = env[Unicorn::Const::PATH_INFO].chomp("/") + if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") + # File exists as-is so serve it up + env[Unicorn::Const::PATH_INFO] = path_info + return @file_server.call(env) + end + + # then try the cached version: + + # grab the semi-colon REST operator used by old versions of Rails + # this is the reason we didn't just copy the new Rails::Rack::Static + env[Unicorn::Const::REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/ + path_info << "#$1#{ActionController::Base.page_cache_extension}" + + if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") + env[Unicorn::Const::PATH_INFO] = path_info + return @file_server.call(env) + end + + @app.call(env) # call OldRails + end +end if defined?(Unicorn::App::OldRails) diff --git a/lib/unicorn/cgi_wrapper.rb b/lib/unicorn/cgi_wrapper.rb new file mode 100644 index 0000000..816b0a0 --- /dev/null +++ b/lib/unicorn/cgi_wrapper.rb @@ -0,0 +1,139 @@ +# This code is based on the original CGIWrapper from Mongrel +# Copyright (c) 2005 Zed A. Shaw +# Copyright (c) 2009 Eric Wong +# You can redistribute it and/or modify it under the same terms as Ruby. +# +# Additional work donated by contributors. See CONTRIBUTORS for more info. + +require 'cgi' + +module Unicorn; end + +# The beginning of a complete wrapper around Unicorn's internal HTTP +# processing system but maintaining the original Ruby CGI module. Use +# this only as a crutch to get existing CGI based systems working. It +# should handle everything, but please notify us if you see special +# warnings. This work is still very alpha so we need testers to help +# work out the various corner cases. +class Unicorn::CGIWrapper < ::CGI + undef_method :env_table + attr_reader :env_table + attr_reader :body + + # these are stripped out of any keys passed to CGIWrapper.header function + NPH = 'nph'.freeze # Completely ignored, Unicorn outputs the date regardless + CONNECTION = 'connection'.freeze # Completely ignored. Why is CGI doing this? + CHARSET = 'charset'.freeze # this gets appended to Content-Type + COOKIE = 'cookie'.freeze # maps (Hash,Array,String) to "Set-Cookie" headers + STATUS = 'status'.freeze # stored as @status + + # some of these are common strings, but this is the only module + # using them and the reason they're not in Unicorn::Const + SET_COOKIE = 'Set-Cookie'.freeze + CONTENT_TYPE = 'Content-Type'.freeze + CONTENT_LENGTH = 'Content-Length'.freeze # this is NOT Const::CONTENT_LENGTH + RACK_INPUT = 'rack.input'.freeze + RACK_ERRORS = 'rack.errors'.freeze + + # this maps CGI header names to HTTP header names + HEADER_MAP = { + 'type' => CONTENT_TYPE, + 'server' => 'Server'.freeze, + 'language' => 'Content-Language'.freeze, + 'expires' => 'Expires'.freeze, + 'length' => CONTENT_LENGTH, + }.freeze + + # Takes an a Rackable environment, plus any additional CGI.new + # arguments These are used internally to create a wrapper around the + # real CGI while maintaining Rack/Unicorn's view of the world. This + # this will NOT deal well with large responses that take up a lot of + # memory, but neither does the CGI nor the original CGIWrapper from + # Mongrel... + def initialize(rack_env, *args) + @env_table = rack_env + @status = 200 + @head = { :cookies => [] } + @body = StringIO.new + super(*args) + end + + # finalizes the response in a way Rack applications would expect + def rack_response + cookies = @head.delete(:cookies) + cookies.empty? or @head[SET_COOKIE] = cookies.join("\n") + @head[CONTENT_LENGTH] ||= @body.size + + [ @status, @head, [ @body.string ] ] + end + + # The header is typically called to send back the header. In our case we + # collect it into a hash for later usage. This can be called multiple + # times to set different cookies. + def header(options = "text/html") + # if they pass in a string then just write the Content-Type + if String === options + @head[CONTENT_TYPE] ||= options + else + HEADER_MAP.each_pair do |from, to| + from = options.delete(from) or next + @head[to] = from + end + + @head[CONTENT_TYPE] ||= "text/html" + if charset = options.delete(CHARSET) + @head[CONTENT_TYPE] << "; charset=#{charset}" + end + + # lots of ways to set cookies + if cookie = options.delete(COOKIE) + cookies = @head[:cookies] + case cookie + when Array + cookie.each { |c| cookies << c.to_s } + when Hash + cookie.each_value { |c| cookies << c.to_s } + else + cookies << cookie.to_s + end + end + @status ||= (status = options.delete(STATUS)) + # drop the keys we don't want anymore + options.delete(NPH) + options.delete(CONNECTION) + + # finally, set the rest of the headers as-is + options.each_pair { |k,v| @head[k] = v } + end + + # doing this fakes out the cgi library to think the headers are empty + # we then do the real headers in the out function call later + "" + end + + # The dumb thing is people can call header or this or both and in + # any order. So, we just reuse header and then finalize the + # HttpResponse the right way. This will have no effect if called + # the second time if the first "outputted" anything. + def out(options = "text/html") + header(options) + @body.size == 0 or return + @body << yield + end + + # Used to wrap the normal stdinput variable used inside CGI. + def stdinput + @env_table[RACK_INPUT] + end + + # The stdoutput should be completely bypassed but we'll drop a + # warning just in case + def stdoutput + err = @env_table[RACK_ERRORS] + err.puts "WARNING: Your program is doing something not expected." + err.puts "Please tell Eric that stdoutput was used and what software " \ + "you are running. Thanks." + @body + end + +end diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index ed7f5b1..4e78171 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -48,10 +48,6 @@ module Unicorn # the constant just refers to a string with the same contents. Using these constants # gave about a 3% to 10% performance improvement over using the strings directly. # Symbols did not really improve things much compared to constants. - # - # While Unicorn does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT, - # REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or - # too taxing on performance. module Const DATE="Date".freeze @@ -61,10 +57,7 @@ module Unicorn # Request body HTTP_BODY="HTTP_BODY".freeze - # This is the initial part that your handler is identified as by URIClassifier. - SCRIPT_NAME="SCRIPT_NAME".freeze - - # The original URI requested by the client. Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME. + # The original URI requested by the client. REQUEST_URI='REQUEST_URI'.freeze REQUEST_PATH='REQUEST_PATH'.freeze @@ -76,14 +69,6 @@ module Unicorn DEFAULT_PORT = "8080".freeze # default TCP listen port DEFAULT_LISTEN = "#{DEFAULT_HOST}:#{DEFAULT_PORT}".freeze - # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff. - ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Unicorn #{UNICORN_VERSION}\r\n\r\nNOT FOUND".freeze - - CONTENT_LENGTH="CONTENT_LENGTH".freeze - - # A common header for indicating the server is too busy. Not used yet. - ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze - # The basic max request size we'll try to read. CHUNK_SIZE=(16 * 1024) @@ -95,22 +80,11 @@ module Unicorn MAX_BODY=MAX_HEADER # A frozen format for this is about 15% faster - CONTENT_TYPE = "Content-Type".freeze - LAST_MODIFIED = "Last-Modified".freeze - ETAG = "ETag".freeze - REQUEST_METHOD="REQUEST_METHOD".freeze - GET="GET".freeze - HEAD="HEAD".freeze - # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32) - ETAG_FORMAT="\"%x-%x-%x\"".freeze - LINE_END="\r\n".freeze + CONTENT_LENGTH="CONTENT_LENGTH".freeze REMOTE_ADDR="REMOTE_ADDR".freeze HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze - HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze - HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze - REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze - HOST = "HOST".freeze - CONNECTION = "Connection".freeze + QUERY_STRING="QUERY_STRING".freeze + RACK_INPUT="rack.input".freeze end end diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index ee407ab..7106f62 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -13,6 +13,20 @@ module Unicorn # class HttpRequest + # default parameters we merge into the request env for Rack handlers + DEF_PARAMS = { + "rack.errors" => $stderr, + "rack.multiprocess" => true, + "rack.multithread" => false, + "rack.run_once" => false, + "rack.url_scheme" => "http", + "rack.version" => [0, 1], + "SCRIPT_NAME" => "", + + # this is not in the Rack spec, but some apps may rely on it + "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}" + }.freeze + def initialize(logger) @logger = logger @body = nil @@ -29,59 +43,39 @@ module Unicorn @body = nil end - # # Does the majority of the IO processing. It has been written in - # Ruby using about 7 different IO processing strategies and no - # matter how it's done the performance just does not improve. It is - # currently carefully constructed to make sure that it gets the best - # possible performance, but anyone who thinks they can make it - # faster is more than welcome to take a crack at it. + # Ruby using about 8 different IO processing strategies. + # + # It is currently carefully constructed to make sure that it gets + # the best possible performance for the common case: GET requests + # that are fully complete after a single read(2) + # + # Anyone who thinks they can make it faster is more than welcome to + # take a crack at it. # # returns an environment hash suitable for Rack if successful # This does minimal exception trapping and it is up to the caller # to handle any socket errors (e.g. user aborted upload). def read(socket) - data = String.new(read_socket(socket)) - nparsed = 0 - - # Assumption: nparsed will always be less since data will get - # filled with more after each parsing. If it doesn't get more - # then there was a problem with the read operation on the client - # socket. Effect is to stop processing when the socket can't - # fill the buffer for further parsing. - while nparsed < data.length - nparsed = @parser.execute(@params, data, nparsed) - - if @parser.finished? - # From http://www.ietf.org/rfc/rfc3875: - # "Script authors should be aware that the REMOTE_ADDR and - # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) - # may not identify the ultimate source of the request. They - # identify the client for the immediate request to the server; - # that client may be a proxy, gateway, or other intermediary - # acting on behalf of the actual source client." - @params[Const::REMOTE_ADDR] = socket.unicorn_peeraddr - - handle_body(socket) and return rack_env # success! - return nil # fail - else - # Parser is not done, queue up more data to read and continue - # parsing - data << read_socket(socket) - if data.length >= Const::MAX_HEADER - raise HttpParserError.new("HEADER is longer than allowed, " \ - "aborting client early.") - end - end + # short circuit the common case with small GET requests first + @parser.execute(@params, read_socket(socket)) and + return handle_body(socket) + + data = @buffer.dup # read_socket will clobber @buffer + + # Parser is not done, queue up more data to read and continue parsing + # an Exception thrown from the @parser will throw us out of the loop + loop do + data << read_socket(socket) + @parser.execute(@params, data) and + return handle_body(socket) end - nil # XXX bug? rescue HttpParserError => e @logger.error "HTTP parse error, malformed request " \ "(#{@params[Const::HTTP_X_FORWARDED_FOR] || socket.unicorn_peeraddr}): #{e.inspect}" @logger.error "REQUEST DATA: #{data.inspect}\n---\n" \ "PARAMS: #{@params.inspect}\n---\n" - socket.closed? or socket.close rescue nil nil end @@ -109,7 +103,7 @@ module Unicorn # This will probably truncate them but at least the request goes through # usually. if remain > 0 - read_body(socket, remain) or return false # fail! + read_body(socket, remain) or return nil # fail! end @body.rewind @body.sysseek(0) if @body.respond_to?(:sysseek) @@ -118,29 +112,37 @@ module Unicorn # another request, we'll truncate it. Again, we don't do pipelining # or keepalive @body.truncate(content_length) - true + rack_env(socket) end # Returns an environment which is rackable: # http://rack.rubyforge.org/doc/files/SPEC.html # Based on Rack's old Mongrel handler. - def rack_env + def rack_env(socket) + # I'm considering enabling "unicorn.client". It gives + # applications some rope to do some "interesting" things like + # replacing a worker with another process that has full control + # over the HTTP response. + # @params["unicorn.client"] = socket + + # From http://www.ietf.org/rfc/rfc3875: + # "Script authors should be aware that the REMOTE_ADDR and + # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) + # may not identify the ultimate source of the request. They + # identify the client for the immediate request to the server; + # that client may be a proxy, gateway, or other intermediary + # acting on behalf of the actual source client." + @params[Const::REMOTE_ADDR] = socket.unicorn_peeraddr + # It might be a dumbass full host request header - @params[Const::REQUEST_PATH] ||= - URI.parse(@params[Const::REQUEST_URI]).path - raise "No REQUEST PATH" unless @params[Const::REQUEST_PATH] - - @params["QUERY_STRING"] ||= '' - @params.update({ "rack.version" => [0,1], - "rack.input" => @body, - "rack.errors" => $stderr, - "rack.multithread" => false, - "rack.multiprocess" => true, - "rack.run_once" => false, - "rack.url_scheme" => "http", - Const::PATH_INFO => @params[Const::REQUEST_PATH], - Const::SCRIPT_NAME => "", - }) + @params[Const::PATH_INFO] = ( + @params[Const::REQUEST_PATH] ||= + URI.parse(@params[Const::REQUEST_URI]).path) or + raise "No REQUEST_PATH" + + @params[Const::QUERY_STRING] ||= '' + @params[Const::RACK_INPUT] = @body + @params.update(DEF_PARAMS) end # Does the heavy lifting of properly reading the larger body requests in diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index c8aa3f9..f928baa 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -35,7 +35,11 @@ module Unicorn # the time anyways so just hope our app knows what it's doing headers.each do |key, value| next if SKIP.include?(key.downcase) - value.split(/\n/).each { |v| out << "#{key}: #{v}" } + if value =~ /\n/ + value.split(/\n/).each { |v| out << "#{key}: #{v}" } + else + out << "#{key}: #{value}" + end end # Rack should enforce Content-Length or chunked transfer encoding, diff --git a/lib/unicorn/socket.rb b/lib/unicorn/socket.rb index 9519448..4870133 100644 --- a/lib/unicorn/socket.rb +++ b/lib/unicorn/socket.rb @@ -62,10 +62,17 @@ module Unicorn end end + def log_buffer_sizes(sock, pfx = '') + respond_to?(:logger) or return + rcvbuf = sock.getsockopt(SOL_SOCKET, SO_RCVBUF).unpack('i') + sndbuf = sock.getsockopt(SOL_SOCKET, SO_SNDBUF).unpack('i') + logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}" + end + # creates a new server, socket. address may be a HOST:PORT or # an absolute path to a UNIX socket. address can even be a Socket # object in which case it is immediately returned - def bind_listen(address = '0.0.0.0:8080', backlog = 1024) + def bind_listen(address = '0.0.0.0:8080', opt = { :backlog => 1024 }) return address unless String === address domain, bind_addr = if address[0..0] == "/" @@ -95,7 +102,13 @@ module Unicorn sock.close rescue nil return nil end - sock.listen(backlog) + if opt[:rcvbuf] || opt[:sndbuf] + log_buffer_sizes(sock, "before: ") + sock.setsockopt(SOL_SOCKET, SO_RCVBUF, opt[:rcvbuf]) if opt[:rcvbuf] + sock.setsockopt(SOL_SOCKET, SO_SNDBUF, opt[:sndbuf]) if opt[:sndbuf] + log_buffer_sizes(sock, " after: ") + end + sock.listen(opt[:backlog] || 1024) set_server_sockopt(sock) if domain == AF_INET sock end diff --git a/test/benchmark/README b/test/benchmark/README new file mode 100644 index 0000000..b63b8a3 --- /dev/null +++ b/test/benchmark/README @@ -0,0 +1,55 @@ += Performance + +Unicorn is pretty fast, and we want it to get faster. Unicorn strives +to get HTTP requests to your application and write HTTP responses back +as quickly as possible. Unicorn does not do any background processing +while your app runs, so your app will get all the CPU time provided to +it by your OS kernel. + +A gentle reminder: Unicorn is NOT for serving clients over slow network +connections. Use nginx (or something similar) to complement Unicorn if +you have slow clients. + +== dd.ru + +This is a pure I/O benchmark. In the context of Unicorn, this is the +only one that matters. It is a standard rackup-compatible .ru file and +may be used with other Rack-compatible servers. + + unicorn -E none dd.ru + +You can change the size and number of chunks in the response with +the "bs" and "count" environment variables. The following command +will cause dd.ru to return 4 chunks of 16384 bytes each, leading to +65536 byte response: + + bs=16384 count=4 unicorn -E none dd.ru + +Or if you want to add logging (small performance impact): + + unicorn -E deployment dd.ru + +Eric runs then runs clients on a LAN it in several different ways: + + client@host1 -> unicorn@host1(tcp) + client@host2 -> unicorn@host1(tcp) + client@host3 -> nginx@host1 -> unicorn@host1(tcp) + client@host3 -> nginx@host1 -> unicorn@host1(unix) + client@host3 -> nginx@host2 -> unicorn@host1(tcp) + +The benchmark client is usually httperf. + +Another gentle reminder: performance with slow networks/clients +is NOT our problem. That is the job of nginx (or similar). + +== request.rb, response.rb, big_request.rb + +These are micro-benchmarks designed to test internal components +of Unicorn. It assumes the internal Unicorn API is mostly stable. + +== Contributors + +This directory is maintained independently in the "benchmark" branch +based against v0.1.0. Only changes to this directory (test/benchmarks) +are committed to this branch although the master branch may merge this +branch occassionaly. diff --git a/test/benchmark/big_request.rb b/test/benchmark/big_request.rb new file mode 100644 index 0000000..5f2111b --- /dev/null +++ b/test/benchmark/big_request.rb @@ -0,0 +1,35 @@ +require 'benchmark' +require 'tempfile' +require 'unicorn' +nr = ENV['nr'] ? ENV['nr'].to_i : 100 +bs = ENV['bs'] ? ENV['bs'].to_i : (1024 * 1024) +count = ENV['count'] ? ENV['count'].to_i : 4 +length = bs * count +slice = (' ' * bs).freeze + +big = Tempfile.new('') +def big.unicorn_peeraddr; '127.0.0.1'; end +big.syswrite( +"PUT /hello/world/puturl?abcd=efg&hi#anchor HTTP/1.0\r\n" \ +"Host: localhost\r\n" \ +"Accept: */*\r\n" \ +"Content-Length: #{length}\r\n" \ +"User-Agent: test-user-agent 0.1.0 (Mozilla compatible) 5.0 asdfadfasda\r\n" \ +"\r\n") +count.times { big.syswrite(slice) } +big.sysseek(0) +big.fsync + +include Unicorn +request = HttpRequest.new(Logger.new($stderr)) + +Benchmark.bmbm do |x| + x.report("big") do + for i in 1..nr + request.read(big) + request.reset + big.sysseek(0) + end + end +end + diff --git a/test/benchmark/dd.ru b/test/benchmark/dd.ru new file mode 100644 index 0000000..111fa2e --- /dev/null +++ b/test/benchmark/dd.ru @@ -0,0 +1,18 @@ +# This benchmark is the simplest test of the I/O facilities in +# unicorn. It is meant to return a fixed-sized blob to test +# the performance of things in Unicorn, _NOT_ the app. +# +# Adjusting this benchmark is done via the "bs" (byte size) and "count" +# environment variables. "count" designates the count of elements of +# "bs" length in the Rack response body. The defaults are bs=4096, count=1 +# to return one 4096-byte chunk. +bs = ENV['bs'] ? ENV['bs'].to_i : 4096 +count = ENV['count'] ? ENV['count'].to_i : 1 +slice = (' ' * bs).freeze +body = (1..count).map { slice }.freeze +hdr = { + 'Content-Length' => (bs * count).to_s.freeze, + 'Content-Type' => 'text/plain'.freeze +}.freeze +response = [ 200, hdr, body ].freeze +run(lambda { |env| response }) diff --git a/test/benchmark/previous.rb b/test/benchmark/previous.rb deleted file mode 100644 index 8b6182a..0000000 --- a/test/benchmark/previous.rb +++ /dev/null @@ -1,11 +0,0 @@ -# Benchmark to compare Mongrel performance against -# previous Mongrel version (the one installed as a gem). -# -# Run with: -# -# ruby previous.rb [num of request] -# - -require File.dirname(__FILE__) + '/utils' - -benchmark "print", %w(current gem), 1000, [1, 10, 100] diff --git a/test/benchmark/request.rb b/test/benchmark/request.rb new file mode 100644 index 0000000..67266cb --- /dev/null +++ b/test/benchmark/request.rb @@ -0,0 +1,47 @@ +require 'benchmark' +require 'unicorn' +nr = ENV['nr'] ? ENV['nr'].to_i : 100000 + +class TestClient + def initialize(response) + @response = (response.join("\r\n") << "\r\n\r\n").freeze + end + def sysread(len, buf) + buf.replace(@response) + end + + def unicorn_peeraddr + '127.0.0.1' + end +end + +small = TestClient.new([ + 'GET / HTTP/1.0', + 'Host: localhost', + 'Accept: */*', + 'User-Agent: test-user-agent 0.1.0' +]) + +medium = TestClient.new([ + 'GET /hello/world/geturl?abcd=efg&hi#anchor HTTP/1.0', + 'Host: localhost', + 'Accept: */*', + 'User-Agent: test-user-agent 0.1.0 (Mozilla compatible) 5.0 asdfadfasda' +]) + +include Unicorn +request = HttpRequest.new(Logger.new($stderr)) +Benchmark.bmbm do |x| + x.report("small") do + for i in 1..nr + request.read(small) + request.reset + end + end + x.report("medium") do + for i in 1..nr + request.read(medium) + request.reset + end + end +end diff --git a/test/benchmark/response.rb b/test/benchmark/response.rb new file mode 100644 index 0000000..0ff0ac2 --- /dev/null +++ b/test/benchmark/response.rb @@ -0,0 +1,29 @@ +require 'benchmark' +require 'unicorn' + +class NullWriter + def syswrite(buf); buf.size; end + def close; end +end + +include Unicorn + +socket = NullWriter.new +bs = ENV['bs'] ? ENV['bs'].to_i : 4096 +count = ENV['count'] ? ENV['count'].to_i : 1 +slice = (' ' * bs).freeze +body = (1..count).map { slice }.freeze +hdr = { + 'Content-Length' => (bs * count).to_s.freeze, + 'Content-Type' => 'text/plain'.freeze +}.freeze +response = [ 200, hdr, body ].freeze + +nr = ENV['nr'] ? ENV['nr'].to_i : 100000 +Benchmark.bmbm do |x| + x.report do + for i in 1..nr + HttpResponse.write(socket.dup, response) + end + end +end diff --git a/test/benchmark/simple.rb b/test/benchmark/simple.rb deleted file mode 100644 index 906f74c..0000000 --- a/test/benchmark/simple.rb +++ /dev/null @@ -1,11 +0,0 @@ -# -# Simple benchmark to compare Mongrel performance against -# other webservers supported by Rack. -# - -require File.dirname(__FILE__) + '/utils' - -libs = %w(current gem WEBrick EMongrel Thin) -libs = ARGV if ARGV.any? - -benchmark "print", libs, 1000, [1, 10, 100] diff --git a/test/benchmark/utils.rb b/test/benchmark/utils.rb deleted file mode 100644 index feb22c1..0000000 --- a/test/benchmark/utils.rb +++ /dev/null @@ -1,82 +0,0 @@ - -require 'rubygems' -require 'rack' -require 'rack/lobster' - -def run(handler_name, n=1000, c=1) - port = 7000 - - server = fork do - [STDOUT, STDERR].each { |o| o.reopen "/dev/null" } - - case handler_name - when 'EMongrel' - require 'swiftcore/evented_mongrel' - handler_name = 'Mongrel' - - when 'Thin' - require 'thin' - hander_name = 'Thin' - - when 'gem' # Load the current Mongrel gem - require 'mongrel' - handler_name = 'Mongrel' - - when 'current' # Load the current Mongrel version under /lib - require File.dirname(__FILE__) + '/../lib/mongrel' - handler_name = 'Mongrel' - - end - - app = Rack::Lobster.new - - handler = Rack::Handler.const_get(handler_name) - handler.run app, :Host => '0.0.0.0', :Port => port - end - - sleep 2 - - out = `nice -n20 ab -c #{c} -n #{n} http://127.0.0.1:#{port}/ 2> /dev/null` - - Process.kill('SIGKILL', server) - Process.wait - - if requests = out.match(/^Requests.+?(\d+\.\d+)/) - requests[1].to_i - else - 0 - end -end - -def benchmark(type, servers, request, concurrency_levels) - send "#{type}_benchmark", servers, request, concurrency_levels -end - -def graph_benchmark(servers, request, concurrency_levels) - require '/usr/local/lib/ruby/gems/1.8/gems/gruff-0.2.9/lib/gruff' - g = Gruff::Area.new - g.title = "Server benchmark" - - servers.each do |server| - g.data(server, concurrency_levels.collect { |c| print '.'; run(server, request, c) }) - end - puts - - g.x_axis_label = 'Concurrency' - g.y_axis_label = 'Requests / sec' - g.labels = {} - concurrency_levels.each_with_index { |c, i| g.labels[i] = c.to_s } - - g.write('bench.png') - `open bench.png` -end - -def print_benchmark(servers, request, concurrency_levels) - puts 'server request concurrency req/s' - puts '=' * 42 - concurrency_levels.each do |c| - servers.each do |server| - puts "#{server.ljust(8)} #{request} #{c.to_s.ljust(4)} #{run(server, request, c)}" - end - end -end
\ No newline at end of file diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index fc75990..1deeaa2 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -14,33 +14,40 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} http = "GET / HTTP/1.1\r\n\r\n" - nread = parser.execute(req, http, 0) - - assert nread == http.length, "Failed to parse the full HTTP request" - assert parser.finished?, "Parser didn't finish" - assert !parser.error?, "Parser had error" - assert nread == parser.nread, "Number read returned from execute does not match" + assert parser.execute(req, http) assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/', req['REQUEST_PATH'] assert_equal 'HTTP/1.1', req['HTTP_VERSION'] assert_equal '/', req['REQUEST_URI'] - assert_equal 'GET', req['REQUEST_METHOD'] + assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_nil req['QUERY_STRING'] parser.reset - assert parser.nread == 0, "Number read after reset should be 0" + req.clear + + assert ! parser.execute(req, "G") + assert req.empty? + + # try parsing again to ensure we were reset correctly + http = "GET /hello-world HTTP/1.1\r\n\r\n" + assert parser.execute(req, http) + + assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_equal '/hello-world', req['REQUEST_PATH'] + assert_equal 'HTTP/1.1', req['HTTP_VERSION'] + assert_equal '/hello-world', req['REQUEST_URI'] + assert_equal 'GET', req['REQUEST_METHOD'] + assert_nil req['FRAGMENT'] + assert_nil req['QUERY_STRING'] end - + def test_parse_strange_headers parser = HttpParser.new req = {} should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" - nread = parser.execute(req, should_be_good, 0) - assert_equal should_be_good.length, nread - assert parser.finished? - assert !parser.error? + assert parser.execute(req, should_be_good) # ref: http://thread.gmane.org/gmane.comp.lang.ruby.Unicorn.devel/37/focus=45 # (note we got 'pen' mixed up with 'pound' in that thread, @@ -49,10 +56,7 @@ class HttpParserTest < Test::Unit::TestCase # nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n" # parser = HttpParser.new # req = {} - # nread = parser.execute(req, nasty_pound_header, 0) - # assert_equal nasty_pound_header.length, nread - # assert parser.finished? - # assert !parser.error? + # assert parser.execute(req, nasty_pound_header, 0) end def test_parse_ie6_urls @@ -66,10 +70,7 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) - nread = parser.execute(req, sorta_safe, 0) - assert_equal sorta_safe.length, nread - assert parser.finished? - assert !parser.error? + assert parser.execute(req, sorta_safe) end end @@ -78,28 +79,68 @@ class HttpParserTest < Test::Unit::TestCase req = {} bad_http = "GET / SsUTF/1.1" - error = false - begin - nread = parser.execute(req, bad_http, 0) - rescue => details - error = true - end + assert_raises(HttpParserError) { parser.execute(req, bad_http) } + parser.reset + assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n")) + end - assert error, "failed to throw exception" - assert !parser.finished?, "Parser shouldn't be finished" - assert parser.error?, "Parser SHOULD have error" + def test_piecemeal + parser = HttpParser.new + req = {} + http = "GET" + assert ! parser.execute(req, http) + assert_raises(HttpParserError) { parser.execute(req, http) } + assert ! parser.execute(req, http << " / HTTP/1.0") + assert_equal '/', req['REQUEST_PATH'] + assert_equal '/', req['REQUEST_URI'] + assert_equal 'GET', req['REQUEST_METHOD'] + assert ! parser.execute(req, http << "\r\n") + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert ! parser.execute(req, http << "\r") + assert parser.execute(req, http << "\n") + assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_nil req['FRAGMENT'] + assert_nil req['QUERY_STRING'] + end + + def test_put_body_oneshot + parser = HttpParser.new + req = {} + http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde" + assert parser.execute(req, http) + assert_equal '/', req['REQUEST_PATH'] + assert_equal '/', req['REQUEST_URI'] + assert_equal 'PUT', req['REQUEST_METHOD'] + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_equal "abcde", req['HTTP_BODY'] + end + + def test_put_body_later + parser = HttpParser.new + req = {} + http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n" + assert parser.execute(req, http) + assert_equal '/l', req['REQUEST_PATH'] + assert_equal '/l', req['REQUEST_URI'] + assert_equal 'PUT', req['REQUEST_METHOD'] + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_equal "", req['HTTP_BODY'] end def test_fragment_in_uri parser = HttpParser.new req = {} get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + ok = false assert_nothing_raised do - parser.execute(req, get, 0) + ok = parser.execute(req, get) end - assert parser.finished? + assert ok assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] + assert_equal 'page=1', req['QUERY_STRING'] end # lame random garbage maker @@ -124,7 +165,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get, 0) + parser.execute({}, get) parser.reset end end @@ -133,7 +174,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get, 0) + parser.execute({}, get) parser.reset end end @@ -142,7 +183,7 @@ class HttpParserTest < Test::Unit::TestCase get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" get << "X-Test: test\r\n" * (80 * 1024) assert_raises Unicorn::HttpParserError do - parser.execute({}, get, 0) + parser.execute({}, get) parser.reset end @@ -150,7 +191,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get, 0) + parser.execute({}, get) parser.reset end end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb new file mode 100644 index 0000000..23fa44c --- /dev/null +++ b/test/unit/test_socket_helper.rb @@ -0,0 +1,159 @@ +require 'test/test_helper' +require 'tempfile' + +class TestSocketHelper < Test::Unit::TestCase + include Unicorn::SocketHelper + attr_reader :logger + GET_SLASH = "GET / HTTP/1.0\r\n\r\n".freeze + + def setup + @log_tmp = Tempfile.new 'logger' + @logger = Logger.new(@log_tmp.path) + @test_addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1' + end + + def test_bind_listen_tcp + port = unused_port @test_addr + @tcp_listener_name = "#@test_addr:#{port}" + @tcp_listener = bind_listen(@tcp_listener_name) + assert Socket === @tcp_listener + assert_equal @tcp_listener_name, sock_name(@tcp_listener) + end + + def test_bind_listen_options + port = unused_port @test_addr + tcp_listener_name = "#@test_addr:#{port}" + tmp = Tempfile.new 'unix.sock' + unix_listener_name = tmp.path + File.unlink(tmp.path) + [ { :backlog => 5 }, { :sndbuf => 4096 }, { :rcvbuf => 4096 }, + { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 } + ].each do |opts| + assert_nothing_raised do + tcp_listener = bind_listen(tcp_listener_name, opts) + assert Socket === tcp_listener + tcp_listener.close + unix_listener = bind_listen(unix_listener_name, opts) + assert Socket === unix_listener + unix_listener.close + end + end + #system('cat', @log_tmp.path) + end + + def test_bind_listen_unix + tmp = Tempfile.new 'unix.sock' + @unix_listener_path = tmp.path + File.unlink(@unix_listener_path) + @unix_listener = bind_listen(@unix_listener_path) + assert Socket === @unix_listener + assert_equal @unix_listener_path, sock_name(@unix_listener) + end + + def test_bind_listen_unix_idempotent + test_bind_listen_unix + a = bind_listen(@unix_listener) + assert_equal a.fileno, @unix_listener.fileno + unix_server = server_cast(@unix_listener) + a = bind_listen(unix_server) + assert_equal a.fileno, unix_server.fileno + assert_equal a.fileno, @unix_listener.fileno + end + + def test_bind_listen_tcp_idempotent + test_bind_listen_tcp + a = bind_listen(@tcp_listener) + assert_equal a.fileno, @tcp_listener.fileno + tcp_server = server_cast(@tcp_listener) + a = bind_listen(tcp_server) + assert_equal a.fileno, tcp_server.fileno + assert_equal a.fileno, @tcp_listener.fileno + end + + def test_bind_listen_unix_rebind + test_bind_listen_unix + new_listener = bind_listen(@unix_listener_path) + assert Socket === new_listener + assert new_listener.fileno != @unix_listener.fileno + assert_equal sock_name(new_listener), sock_name(@unix_listener) + assert_equal @unix_listener_path, sock_name(new_listener) + pid = fork do + client = server_cast(new_listener).accept + client.syswrite('abcde') + exit 0 + end + s = UNIXSocket.new(@unix_listener_path) + IO.select([s]) + assert_equal 'abcde', s.sysread(5) + pid, status = Process.waitpid2(pid) + assert status.success? + end + + def test_server_cast + assert_nothing_raised do + test_bind_listen_unix + test_bind_listen_tcp + end + @unix_server = server_cast(@unix_listener) + assert_equal @unix_listener.fileno, @unix_server.fileno + assert UNIXServer === @unix_server + assert File.socket?(@unix_server.path) + assert_equal @unix_listener_path, sock_name(@unix_server) + + @tcp_server = server_cast(@tcp_listener) + assert_equal @tcp_listener.fileno, @tcp_server.fileno + assert TCPServer === @tcp_server + assert_equal @tcp_listener_name, sock_name(@tcp_server) + end + + def test_sock_name + test_server_cast + sock_name(@unix_server) + end + + def test_tcp_unicorn_peeraddr + test_bind_listen_tcp + @tcp_server = server_cast(@tcp_listener) + tmp = Tempfile.new 'shared' + pid = fork do + client = @tcp_server.accept + IO.select([client]) + assert_equal GET_SLASH, client.sysread(GET_SLASH.size) + tmp.syswrite "#{client.unicorn_peeraddr}" + exit 0 + end + host, port = sock_name(@tcp_server).split(/:/) + client = TCPSocket.new(host, port.to_i) + client.syswrite(GET_SLASH) + + pid, status = Process.waitpid2(pid) + assert_nothing_raised { client.close } + assert status.success? + tmp.sysseek 0 + assert_equal @test_addr, tmp.sysread(4096) + tmp.sysseek 0 + end + + def test_unix_unicorn_peeraddr + test_bind_listen_unix + @unix_server = server_cast(@unix_listener) + tmp = Tempfile.new 'shared' + pid = fork do + client = @unix_server.accept + IO.select([client]) + assert_equal GET_SLASH, client.sysread(4096) + tmp.syswrite "#{client.unicorn_peeraddr}" + exit 0 + end + client = UNIXSocket.new(@unix_listener_path) + client.syswrite(GET_SLASH) + + pid, status = Process.waitpid2(pid) + assert_nothing_raised { client.close } + assert status.success? + tmp.sysseek 0 + assert_equal '127.0.0.1', tmp.sysread(4096) + tmp.sysseek 0 + end + +end |