unicorn 0.5.1 → 0.5.2

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