unicorn 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,4 @@
1
+ v0.5.2 - force Status: header for compat, small cleanups
1
2
  v0.5.1 - exit correctly on INT/TERM, QUIT is still recommended, however
2
3
  v0.5.0 - {after,before}_fork API change, small tweaks/fixes
3
4
  v0.4.2 - fix Rails ARStore, FD leak prevention, descriptive proctitles
@@ -32,23 +32,23 @@ inst_deps := $(wildcard bin/*) $(wildcard lib/*.rb) \
32
32
 
33
33
  ext/unicorn/http11/http11_parser.c: ext/unicorn/http11/http11_parser.rl
34
34
  cd $(@D) && ragel $(<F) -C -G2 -o $(@F)
35
- ext/unicorn/http11/Makefile: ext/unicorn/http11/extconf.rb
35
+ ext/unicorn/http11/Makefile: ext/unicorn/http11/extconf.rb $(http11_deps)
36
36
  cd $(@D) && $(ruby) $(<F)
37
- ext/unicorn/http11/http11.$(DLEXT): $(http11_deps) ext/unicorn/http11/Makefile
37
+ ext/unicorn/http11/http11.$(DLEXT): ext/unicorn/http11/Makefile
38
38
  $(MAKE) -C $(@D)
39
39
  lib/unicorn/http11.$(DLEXT): ext/unicorn/http11/http11.$(DLEXT)
40
40
  @mkdir -p lib
41
41
  install -m644 $< $@
42
42
  http11: lib/unicorn/http11.$(DLEXT)
43
43
 
44
- $(test_prefix)/.stamp: $(inst_deps)
45
- $(MAKE) clean-http11
46
- $(MAKE) install-test
44
+ $(test_prefix)/.stamp: install-test
47
45
  > $@
48
46
 
49
- install-test:
47
+ install-test: $(inst_deps)
48
+ test -n "$(test_prefix)"
50
49
  mkdir -p $(test_prefix)/.ccache
51
50
  tar c bin ext lib GNUmakefile | (cd $(test_prefix) && tar x)
51
+ $(MAKE) -C $(test_prefix) clean
52
52
  $(MAKE) -C $(test_prefix) http11 shebang
53
53
 
54
54
  # this is only intended to be run within $(test_prefix)
@@ -62,7 +62,7 @@ test: $(T) $(T_n)
62
62
 
63
63
  test-exec: $(wildcard test/exec/test_*.rb)
64
64
  test-unit: $(wildcard test/unit/test_*.rb)
65
- $(slow_tests):
65
+ $(slow_tests): $(test_prefix)/.stamp
66
66
  @$(MAKE) $(shell $(awk_slow) $@)
67
67
 
68
68
  TEST_OPTS = -v
@@ -40,6 +40,7 @@ static VALUE global_server_protocol_value;
40
40
  static VALUE global_http_host;
41
41
  static VALUE global_http_x_forwarded_proto;
42
42
  static VALUE global_port_80;
43
+ static VALUE global_port_443;
43
44
  static VALUE global_localhost;
44
45
  static VALUE global_http;
45
46
 
@@ -243,40 +244,36 @@ static void http_version(void *data, const char *at, size_t length)
243
244
  rb_hash_aset(req, global_http_version, val);
244
245
  }
245
246
 
246
- /** Finalizes the request header to have a bunch of stuff that's
247
- needed. */
248
-
247
+ /** Finalizes the request header to have a bunch of stuff that's needed. */
249
248
  static void header_done(void *data, const char *at, size_t length)
250
249
  {
251
250
  VALUE req = (VALUE)data;
252
- VALUE temp = Qnil;
253
- char *colon = NULL;
251
+ VALUE server_name = global_localhost;
252
+ VALUE server_port = global_port_80;
253
+ VALUE temp;
254
254
 
255
255
  /* set rack.url_scheme to "https" or "http", no others are allowed by Rack */
256
- temp = rb_hash_aref(req, global_http_x_forwarded_proto);
257
- switch (temp == Qnil ? 0 : RSTRING_LEN(temp)) {
258
- case 5: if (!memcmp("https", RSTRING_PTR(temp), 5)) break;
259
- case 4: if (!memcmp("http", RSTRING_PTR(temp), 4)) break;
260
- default: temp = global_http;
261
- }
256
+ if ((temp = rb_hash_aref(req, global_http_x_forwarded_proto)) != Qnil &&
257
+ RSTRING_LEN(temp) == 5 &&
258
+ !memcmp("https", RSTRING_PTR(temp), 5))
259
+ server_port = global_port_443;
260
+ else
261
+ temp = global_http;
262
262
  rb_hash_aset(req, global_rack_url_scheme, temp);
263
263
 
264
- /* set the SERVER_NAME and SERVER_PORT variables */
265
- if((temp = rb_hash_aref(req, global_http_host)) != Qnil) {
266
- colon = memchr(RSTRING_PTR(temp), ':', RSTRING_LEN(temp));
267
- if(colon != NULL) {
268
- rb_hash_aset(req, global_server_name, rb_str_substr(temp, 0, colon - RSTRING_PTR(temp)));
269
- rb_hash_aset(req, global_server_port,
270
- rb_str_substr(temp, colon - RSTRING_PTR(temp)+1,
271
- RSTRING_LEN(temp)));
264
+ /* parse and set the SERVER_NAME and SERVER_PORT variables */
265
+ if ((temp = rb_hash_aref(req, global_http_host)) != Qnil) {
266
+ char *colon = memchr(RSTRING_PTR(temp), ':', RSTRING_LEN(temp));
267
+ if (colon) {
268
+ server_name = rb_str_substr(temp, 0, colon - RSTRING_PTR(temp));
269
+ server_port = rb_str_substr(temp, colon - RSTRING_PTR(temp)+1,
270
+ RSTRING_LEN(temp));
272
271
  } else {
273
- rb_hash_aset(req, global_server_name, temp);
274
- rb_hash_aset(req, global_server_port, global_port_80);
272
+ server_name = temp;
275
273
  }
276
- } else {
277
- rb_hash_aset(req, global_server_name, global_localhost);
278
- rb_hash_aset(req, global_server_port, global_port_80);
279
274
  }
275
+ rb_hash_aset(req, global_server_name, server_name);
276
+ rb_hash_aset(req, global_server_port, server_port);
280
277
 
281
278
  /* grab the initial body and stuff it into the hash */
282
279
  rb_hash_aset(req, sym_http_body, rb_str_new(at, length));
@@ -402,6 +399,7 @@ void Init_http11()
402
399
  DEF_GLOBAL(http_host, "HTTP_HOST");
403
400
  DEF_GLOBAL(http_x_forwarded_proto, "HTTP_X_FORWARDED_PROTO");
404
401
  DEF_GLOBAL(port_80, "80");
402
+ DEF_GLOBAL(port_443, "443");
405
403
  DEF_GLOBAL(localhost, "localhost");
406
404
  DEF_GLOBAL(http, "http");
407
405
 
@@ -52,7 +52,6 @@ module Unicorn
52
52
  @start_ctx = DEFAULT_START_CTX.dup
53
53
  @start_ctx.merge!(start_ctx) if start_ctx
54
54
  @app = app
55
- @master_pid = $$
56
55
  @workers = Hash.new
57
56
  @io_purgatory = [] # prevents IO objects in here from being GC-ed
58
57
  @request = @rd_sig = @wr_sig = nil
@@ -193,9 +192,9 @@ module Unicorn
193
192
  stop(false)
194
193
  break
195
194
  when :USR1 # rotate logs
196
- logger.info "master rotating logs..."
195
+ logger.info "master reopening logs..."
197
196
  Unicorn::Util.reopen_logs
198
- logger.info "master done rotating logs"
197
+ logger.info "master done reopening logs"
199
198
  kill_each_worker(:USR1)
200
199
  when :USR2 # exec binary, stay alive in case something went wrong
201
200
  reexec
@@ -353,7 +352,7 @@ module Unicorn
353
352
  io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
354
353
  end
355
354
  logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
356
- @before_exec.call(self) if @before_exec
355
+ @before_exec.call(self)
357
356
  exec(*cmd)
358
357
  end
359
358
  proc_name 'master (old)'
@@ -438,7 +437,7 @@ module Unicorn
438
437
  @start_ctx = @workers = @rd_sig = @wr_sig = nil
439
438
  @listeners.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
440
439
  worker.tempfile.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
441
- @after_fork.call(self, worker) if @after_fork # can drop perms
440
+ @after_fork.call(self, worker) # can drop perms
442
441
  @request = HttpRequest.new(logger)
443
442
  build_app! unless @preload_app
444
443
  end
@@ -447,28 +446,27 @@ module Unicorn
447
446
  # for connections and doesn't die until the parent dies (or is
448
447
  # given a INT, QUIT, or TERM signal)
449
448
  def worker_loop(worker)
449
+ master_pid = Process.ppid # slightly racy, but less memory usage
450
450
  init_worker_process(worker)
451
- nr = 0
451
+ nr = 0 # this becomes negative if we need to reopen logs
452
452
  tempfile = worker.tempfile
453
- alive = true
454
453
  ready = @listeners
455
454
  client = nil
456
- trap(:QUIT) do
457
- alive = false # graceful shutdown
458
- @listeners.each { |sock| sock.close rescue nil } # break IO.select
459
- end
460
- reopen_logs, (rd, wr) = false, IO.pipe
455
+ rd, wr = IO.pipe
461
456
  rd.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
462
457
  wr.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
463
- trap(:USR1) { reopen_logs = true; rd.close rescue nil } # break IO.select
458
+
459
+ # closing anything we IO.select on will raise EBADF
460
+ trap(:USR1) { nr = -65536; rd.close rescue nil }
461
+ trap(:QUIT) { @listeners.each { |sock| sock.close rescue nil } }
462
+ [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown
464
463
  @logger.info "worker=#{worker.nr} ready"
465
464
 
466
- while alive && @master_pid == Process.ppid
467
- if reopen_logs
468
- reopen_logs = false
469
- @logger.info "worker=#{worker.nr} rotating logs..."
465
+ while master_pid == Process.ppid
466
+ if nr < 0
467
+ @logger.info "worker=#{worker.nr} reopening logs..."
470
468
  Unicorn::Util.reopen_logs
471
- @logger.info "worker=#{worker.nr} done rotating logs"
469
+ @logger.info "worker=#{worker.nr} done reopening logs"
472
470
  wr.close rescue nil
473
471
  rd, wr = IO.pipe
474
472
  rd.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
@@ -480,11 +478,11 @@ module Unicorn
480
478
  # prefer temporary files to be unlinked for security,
481
479
  # performance and reliability reasons, so utime is out. No-op
482
480
  # changes with chmod doesn't update ctime on all filesystems; so
483
- # we increment our counter each and every time.
484
- tempfile.chmod(nr += 1)
481
+ # we change our counter each and every time (after process_client
482
+ # and before IO.select).
483
+ tempfile.chmod(nr = 0)
485
484
 
486
485
  begin
487
- accepted = false
488
486
  ready.each do |sock|
489
487
  begin
490
488
  client = begin
@@ -492,22 +490,22 @@ module Unicorn
492
490
  rescue Errno::EAGAIN
493
491
  next
494
492
  end
495
- accepted = true
496
493
  process_client(client)
497
494
  rescue Errno::ECONNABORTED
498
495
  # client closed the socket even before accept
499
496
  client.close rescue nil
497
+ ensure
498
+ tempfile.chmod(nr += 1)
499
+ break if nr < 0
500
500
  end
501
- tempfile.chmod(nr += 1)
502
- break if reopen_logs
503
501
  end
504
502
  client = nil
505
503
 
506
504
  # make the following bet: if we accepted clients this round,
507
- # we're probably reasonably busy, so avoid calling select(2)
508
- # and try to do a blind non-blocking accept(2) on everything
509
- # before we sleep again in select
510
- if accepted || reopen_logs
505
+ # we're probably reasonably busy, so avoid calling select()
506
+ # and do a speculative accept_nonblock on every listener
507
+ # before we sleep again in select().
508
+ if nr != 0 # (nr < 0) => reopen logs
511
509
  ready = @listeners
512
510
  else
513
511
  begin
@@ -518,7 +516,7 @@ module Unicorn
518
516
  rescue Errno::EINTR
519
517
  ready = @listeners
520
518
  rescue Errno::EBADF => e
521
- reopen_logs or exit(alive ? 1 : 0)
519
+ nr < 0 or exit(@listeners[0].closed? ? 0 : 1)
522
520
  end
523
521
  end
524
522
  rescue SignalException, SystemExit => e
@@ -4,43 +4,43 @@ module Unicorn
4
4
  # Every standard HTTP code mapped to the appropriate message. These are
5
5
  # used so frequently that they are placed directly in Unicorn for easy
6
6
  # access rather than Unicorn::Const itself.
7
- HTTP_STATUS_CODES = {
8
- 100 => 'Continue',
9
- 101 => 'Switching Protocols',
10
- 200 => 'OK',
11
- 201 => 'Created',
12
- 202 => 'Accepted',
13
- 203 => 'Non-Authoritative Information',
14
- 204 => 'No Content',
15
- 205 => 'Reset Content',
16
- 206 => 'Partial Content',
17
- 300 => 'Multiple Choices',
18
- 301 => 'Moved Permanently',
19
- 302 => 'Moved Temporarily',
20
- 303 => 'See Other',
21
- 304 => 'Not Modified',
22
- 305 => 'Use Proxy',
23
- 400 => 'Bad Request',
24
- 401 => 'Unauthorized',
25
- 402 => 'Payment Required',
26
- 403 => 'Forbidden',
27
- 404 => 'Not Found',
28
- 405 => 'Method Not Allowed',
29
- 406 => 'Not Acceptable',
30
- 407 => 'Proxy Authentication Required',
31
- 408 => 'Request Time-out',
32
- 409 => 'Conflict',
33
- 410 => 'Gone',
34
- 411 => 'Length Required',
35
- 412 => 'Precondition Failed',
36
- 413 => 'Request Entity Too Large',
37
- 414 => 'Request-URI Too Large',
38
- 415 => 'Unsupported Media Type',
39
- 500 => 'Internal Server Error',
40
- 501 => 'Not Implemented',
41
- 502 => 'Bad Gateway',
42
- 503 => 'Service Unavailable',
43
- 504 => 'Gateway Time-out',
7
+ HTTP_STATUS_CODES = {
8
+ 100 => 'Continue',
9
+ 101 => 'Switching Protocols',
10
+ 200 => 'OK',
11
+ 201 => 'Created',
12
+ 202 => 'Accepted',
13
+ 203 => 'Non-Authoritative Information',
14
+ 204 => 'No Content',
15
+ 205 => 'Reset Content',
16
+ 206 => 'Partial Content',
17
+ 300 => 'Multiple Choices',
18
+ 301 => 'Moved Permanently',
19
+ 302 => 'Moved Temporarily',
20
+ 303 => 'See Other',
21
+ 304 => 'Not Modified',
22
+ 305 => 'Use Proxy',
23
+ 400 => 'Bad Request',
24
+ 401 => 'Unauthorized',
25
+ 402 => 'Payment Required',
26
+ 403 => 'Forbidden',
27
+ 404 => 'Not Found',
28
+ 405 => 'Method Not Allowed',
29
+ 406 => 'Not Acceptable',
30
+ 407 => 'Proxy Authentication Required',
31
+ 408 => 'Request Time-out',
32
+ 409 => 'Conflict',
33
+ 410 => 'Gone',
34
+ 411 => 'Length Required',
35
+ 412 => 'Precondition Failed',
36
+ 413 => 'Request Entity Too Large',
37
+ 414 => 'Request-URI Too Large',
38
+ 415 => 'Unsupported Media Type',
39
+ 500 => 'Internal Server Error',
40
+ 501 => 'Not Implemented',
41
+ 502 => 'Bad Gateway',
42
+ 503 => 'Service Unavailable',
43
+ 504 => 'Gateway Time-out',
44
44
  505 => 'HTTP Version not supported'
45
45
  }
46
46
 
@@ -53,12 +53,12 @@ module Unicorn
53
53
 
54
54
  # This is the part of the path after the SCRIPT_NAME.
55
55
  PATH_INFO="PATH_INFO".freeze
56
-
56
+
57
57
  # The original URI requested by the client.
58
58
  REQUEST_URI='REQUEST_URI'.freeze
59
59
  REQUEST_PATH='REQUEST_PATH'.freeze
60
-
61
- UNICORN_VERSION="0.5.1".freeze
60
+
61
+ UNICORN_VERSION="0.5.2".freeze
62
62
 
63
63
  UNICORN_TMP_BASE="unicorn".freeze
64
64
 
@@ -24,12 +24,16 @@ module Unicorn
24
24
  # Rack does not set/require a Date: header. We always override the
25
25
  # Connection: and Date: headers no matter what (if anything) our
26
26
  # Rack application sent us.
27
- SKIP = { 'connection' => true, 'date' => true }.freeze
27
+ SKIP = { 'connection' => true, 'date' => true, 'status' => true }.freeze
28
28
 
29
29
  # writes the rack_response to socket as an HTTP response
30
30
  def self.write(socket, rack_response)
31
31
  status, headers, body = rack_response
32
- out = [ "Date: #{Time.now.httpdate}" ]
32
+ status = "#{status} #{HTTP_STATUS_CODES[status]}"
33
+
34
+ # Date is required by HTTP/1.1 as long as our clock can be trusted.
35
+ # Some broken clients require a "Status" header so we accomodate them
36
+ out = [ "Date: #{Time.now.httpdate}", "Status: #{status}" ]
33
37
 
34
38
  # Don't bother enforcing duplicate supression, it's a Hash most of
35
39
  # the time anyways so just hope our app knows what it's doing
@@ -45,7 +49,7 @@ module Unicorn
45
49
  # Rack should enforce Content-Length or chunked transfer encoding,
46
50
  # so don't worry or care about them.
47
51
  socket_write(socket,
48
- "HTTP/1.1 #{status} #{HTTP_STATUS_CODES[status]}\r\n" \
52
+ "HTTP/1.1 #{status}\r\n" \
49
53
  "Connection: close\r\n" \
50
54
  "#{out.join("\r\n")}\r\n\r\n")
51
55
  body.each { |chunk| socket_write(socket, chunk) }
@@ -370,21 +370,21 @@ end
370
370
  tries = DEFAULT_TRIES
371
371
  log = File.readlines(rotate.path)
372
372
  while (tries -= 1) > 0 &&
373
- log.grep(/rotating logs\.\.\./).size < 5
373
+ log.grep(/reopening logs\.\.\./).size < 5
374
374
  sleep DEFAULT_RES
375
375
  log = File.readlines(rotate.path)
376
376
  end
377
- assert_equal 5, log.grep(/rotating logs\.\.\./).size
378
- assert_equal 0, log.grep(/done rotating logs/).size
377
+ assert_equal 5, log.grep(/reopening logs\.\.\./).size
378
+ assert_equal 0, log.grep(/done reopening logs/).size
379
379
 
380
380
  tries = DEFAULT_TRIES
381
381
  log = File.readlines(COMMON_TMP.path)
382
- while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 5
382
+ while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
383
383
  sleep DEFAULT_RES
384
384
  log = File.readlines(COMMON_TMP.path)
385
385
  end
386
- assert_equal 5, log.grep(/done rotating logs/).size
387
- assert_equal 0, log.grep(/rotating logs\.\.\./).size
386
+ assert_equal 5, log.grep(/done reopening logs/).size
387
+ assert_equal 0, log.grep(/reopening logs\.\.\./).size
388
388
  assert_nothing_raised { Process.kill(:QUIT, pid) }
389
389
  status = nil
390
390
  assert_nothing_raised { pid, status = Process.waitpid2(pid) }
@@ -54,6 +54,27 @@ class ResponseTest < Test::Unit::TestCase
54
54
  assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string)
55
55
  end
56
56
 
57
+ # Even though Rack explicitly forbids "Status" in the header hash,
58
+ # some broken clients still rely on it
59
+ def test_status_header_added
60
+ out = StringIO.new
61
+ HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, []])
62
+ assert out.closed?
63
+ assert_match(/^Status: 200 OK\r\nX-Whatever: stuff\r\n/, out.string)
64
+ end
65
+
66
+ # we always favor the code returned by the application, since "Status"
67
+ # in the header hash is not allowed by Rack (but not every app is
68
+ # fully Rack-compliant).
69
+ def test_status_header_ignores_app_hash
70
+ out = StringIO.new
71
+ header_hash = {"X-Whatever" => "stuff", 'StaTus' => "666" }
72
+ HttpResponse.write(out,[200, header_hash, []])
73
+ assert out.closed?
74
+ assert_match(/^Status: 200 OK\r\nX-Whatever: stuff\r\n/, out.string)
75
+ assert_equal 1, out.string.split(/\r\n/).grep(/^Status:/i).size
76
+ end
77
+
57
78
  def test_body_closed
58
79
  expect_body = %w(1 2 3 4).join("\n")
59
80
  body = StringIO.new(expect_body)
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{unicorn}
5
- s.version = "0.5.1"
5
+ s.version = "0.5.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Eric Wong"]
9
- s.date = %q{2009-04-13}
9
+ s.date = %q{2009-04-16}
10
10
  s.description = %q{A small fast HTTP library and server for Rack applications.}
11
11
  s.email = %q{normalperson@yhbt.net}
12
12
  s.executables = ["unicorn", "unicorn_rails"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Wong
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-04-13 00:00:00 -07:00
12
+ date: 2009-04-16 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15