unicorn 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -65,7 +65,7 @@ module Unicorn
65
65
  def commit!(server, options = {}) #:nodoc:
66
66
  skip = options[:skip] || []
67
67
  @set.each do |key, value|
68
- (Symbol === value && value == :unset) and next
68
+ value == :unset and next
69
69
  skip.include?(key) and next
70
70
  setter = "#{key}="
71
71
  if server.respond_to?(setter)
data/lib/unicorn/const.rb CHANGED
@@ -5,7 +5,7 @@ module Unicorn
5
5
  # gave about a 3% to 10% performance improvement over using the strings directly.
6
6
  # Symbols did not really improve things much compared to constants.
7
7
  module Const
8
- UNICORN_VERSION="0.8.4".freeze
8
+ UNICORN_VERSION="0.9.0".freeze
9
9
 
10
10
  DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address
11
11
  DEFAULT_PORT = "8080".freeze # default TCP listen port
@@ -24,11 +24,15 @@ module Unicorn
24
24
  # common errors we'll send back
25
25
  ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
26
26
  ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
27
+ EXPECT_100_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
27
28
 
28
29
  # A frozen format for this is about 15% faster
30
+ HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
29
31
  CONTENT_LENGTH="CONTENT_LENGTH".freeze
30
32
  REMOTE_ADDR="REMOTE_ADDR".freeze
31
33
  HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze
34
+ HTTP_EXPECT="HTTP_EXPECT".freeze
35
+ HTTP_TRAILER="HTTP_TRAILER".freeze
32
36
  RACK_INPUT="rack.input".freeze
33
37
  end
34
38
 
@@ -1,15 +1,9 @@
1
- require 'tempfile'
2
1
  require 'stringio'
3
2
 
4
3
  # compiled extension
5
4
  require 'unicorn/http11'
6
5
 
7
6
  module Unicorn
8
- #
9
- # The HttpRequest.initialize method will convert any request that is larger than
10
- # Const::MAX_BODY into a Tempfile and use that as the body. Otherwise it uses
11
- # a StringIO object. To be safe, you should assume it works like a file.
12
- #
13
7
  class HttpRequest
14
8
 
15
9
  attr_accessor :logger
@@ -27,14 +21,13 @@ module Unicorn
27
21
  "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze
28
22
  }
29
23
 
30
- # Optimize for the common case where there's no request body
31
- # (GET/HEAD) requests.
32
- NULL_IO = StringIO.new
24
+ NULL_IO = StringIO.new(Z)
33
25
  LOCALHOST = '127.0.0.1'.freeze
34
26
 
35
27
  # Being explicitly single-threaded, we have certain advantages in
36
28
  # not having to worry about variables being clobbered :)
37
29
  BUFFER = ' ' * Const::CHUNK_SIZE # initial size, may grow
30
+ BUFFER.force_encoding(Encoding::BINARY) if Z.respond_to?(:force_encoding)
38
31
  PARSER = HttpParser.new
39
32
  PARAMS = Hash.new
40
33
 
@@ -56,11 +49,6 @@ module Unicorn
56
49
  # This does minimal exception trapping and it is up to the caller
57
50
  # to handle any socket errors (e.g. user aborted upload).
58
51
  def read(socket)
59
- # reset the parser
60
- unless NULL_IO == (input = PARAMS[Const::RACK_INPUT]) # unlikely
61
- input.close rescue nil
62
- input.close! rescue nil
63
- end
64
52
  PARAMS.clear
65
53
  PARSER.reset
66
54
 
@@ -98,57 +86,25 @@ module Unicorn
98
86
  private
99
87
 
100
88
  # Handles dealing with the rest of the request
101
- # returns a Rack environment if successful, raises an exception if not
89
+ # returns a Rack environment if successful
102
90
  def handle_body(socket)
103
- http_body = PARAMS.delete(:http_body)
104
- content_length = PARAMS[Const::CONTENT_LENGTH].to_i
105
-
106
- if content_length == 0 # short circuit the common case
107
- PARAMS[Const::RACK_INPUT] = NULL_IO.closed? ? NULL_IO.reopen : NULL_IO
108
- return PARAMS.update(DEFAULTS)
91
+ PARAMS[Const::RACK_INPUT] = if (body = PARAMS.delete(:http_body))
92
+ length = PARAMS[Const::CONTENT_LENGTH].to_i
93
+
94
+ if te = PARAMS[Const::HTTP_TRANSFER_ENCODING]
95
+ if /\Achunked\z/i =~ te
96
+ socket = ChunkedReader.new(PARAMS, socket, body)
97
+ length = body = nil
98
+ end
99
+ end
100
+
101
+ TeeInput.new(socket, length, body)
102
+ else
103
+ NULL_IO.closed? ? NULL_IO.reopen(Z) : NULL_IO
109
104
  end
110
105
 
111
- # must read more data to complete body
112
- remain = content_length - http_body.length
113
-
114
- body = PARAMS[Const::RACK_INPUT] = (remain < Const::MAX_BODY) ?
115
- StringIO.new : Tempfile.new('unicorn')
116
-
117
- body.binmode
118
- body.write(http_body)
119
-
120
- # Some clients (like FF1.0) report 0 for body and then send a body.
121
- # This will probably truncate them but at least the request goes through
122
- # usually.
123
- read_body(socket, remain, body) if remain > 0
124
- body.rewind
125
-
126
- # in case read_body overread because the client tried to pipeline
127
- # another request, we'll truncate it. Again, we don't do pipelining
128
- # or keepalive
129
- body.truncate(content_length)
130
106
  PARAMS.update(DEFAULTS)
131
107
  end
132
108
 
133
- # Does the heavy lifting of properly reading the larger body
134
- # requests in small chunks. It expects PARAMS['rack.input'] to be
135
- # an IO object, socket to be valid, It also expects any initial part
136
- # of the body that has been read to be in the PARAMS['rack.input']
137
- # already. It will return true if successful and false if not.
138
- def read_body(socket, remain, body)
139
- begin
140
- # write always writes the requested amount on a POSIX filesystem
141
- remain -= body.write(socket.readpartial(Const::CHUNK_SIZE, BUFFER))
142
- end while remain > 0
143
- rescue Object => e
144
- @logger.error "Error reading HTTP body: #{e.inspect}"
145
-
146
- # Any errors means we should delete the file, including if the file
147
- # is dumped. Truncate it ASAP to help avoid page flushes to disk.
148
- body.truncate(0) rescue nil
149
- reset
150
- raise e
151
- end
152
-
153
109
  end
154
110
  end
@@ -31,13 +31,12 @@ module Unicorn
31
31
  # Connection: and Date: headers no matter what (if anything) our
32
32
  # Rack application sent us.
33
33
  SKIP = { 'connection' => true, 'date' => true, 'status' => true }.freeze
34
- EMPTY = ''.freeze # :nodoc
35
34
  OUT = [] # :nodoc
36
35
 
37
36
  # writes the rack_response to socket as an HTTP response
38
37
  def self.write(socket, rack_response)
39
38
  status, headers, body = rack_response
40
- status = CODES[status.to_i] || status
39
+ status = CODES[status.to_i]
41
40
  OUT.clear
42
41
 
43
42
  # Don't bother enforcing duplicate supression, it's a Hash most of
@@ -59,7 +58,7 @@ module Unicorn
59
58
  "Date: #{Time.now.httpdate}\r\n" \
60
59
  "Status: #{status}\r\n" \
61
60
  "Connection: close\r\n" \
62
- "#{OUT.join(EMPTY)}\r\n")
61
+ "#{OUT.join(Z)}\r\n")
63
62
  body.each { |chunk| socket.write(chunk) }
64
63
  socket.close # flushes and uncorks the socket immediately
65
64
  ensure
@@ -0,0 +1,135 @@
1
+ # Copyright (c) 2009 Eric Wong
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+
4
+ require 'tempfile'
5
+
6
+ # acts like tee(1) on an input input to provide a input-like stream
7
+ # while providing rewindable semantics through a Tempfile/StringIO
8
+ # backing store. On the first pass, the input is only read on demand
9
+ # so your Rack application can use input notification (upload progress
10
+ # and like). This should fully conform to the Rack::InputWrapper
11
+ # specification on the public API. This class is intended to be a
12
+ # strict interpretation of Rack::InputWrapper functionality and will
13
+ # not support any deviations from it.
14
+
15
+ module Unicorn
16
+ class TeeInput
17
+
18
+ def initialize(input, size, body)
19
+ @tmp = Tempfile.new(nil)
20
+ @tmp.unlink
21
+ @tmp.binmode
22
+ @tmp.sync = true
23
+
24
+ if body
25
+ @tmp.write(body)
26
+ @tmp.seek(0)
27
+ end
28
+ @input = input
29
+ @size = size # nil if chunked
30
+ end
31
+
32
+ # returns the size of the input. This is what the Content-Length
33
+ # header value should be, and how large our input is expected to be.
34
+ # For TE:chunked, this requires consuming all of the input stream
35
+ # before returning since there's no other way
36
+ def size
37
+ @size and return @size
38
+
39
+ if @input
40
+ buf = Z.dup
41
+ while tee(Const::CHUNK_SIZE, buf)
42
+ end
43
+ @tmp.rewind
44
+ end
45
+
46
+ @size = @tmp.stat.size
47
+ end
48
+
49
+ def read(*args)
50
+ @input or return @tmp.read(*args)
51
+
52
+ length = args.shift
53
+ if nil == length
54
+ rv = @tmp.read || Z.dup
55
+ tmp = Z.dup
56
+ while tee(Const::CHUNK_SIZE, tmp)
57
+ rv << tmp
58
+ end
59
+ rv
60
+ else
61
+ buf = args.shift || Z.dup
62
+ diff = @tmp.stat.size - @tmp.pos
63
+ if 0 == diff
64
+ tee(length, buf)
65
+ else
66
+ @tmp.read(diff > length ? length : diff, buf)
67
+ end
68
+ end
69
+ end
70
+
71
+ # takes zero arguments for strict Rack::Lint compatibility, unlike IO#gets
72
+ def gets
73
+ @input or return @tmp.gets
74
+ nil == $/ and return read
75
+
76
+ line = nil
77
+ if @tmp.pos < @tmp.stat.size
78
+ line = @tmp.gets # cannot be nil here
79
+ $/ == line[-$/.size, $/.size] and return line
80
+
81
+ # half the line was already read, and the rest of has not been read
82
+ if buf = @input.gets
83
+ @tmp.write(buf)
84
+ line << buf
85
+ else
86
+ @input = nil
87
+ end
88
+ elsif line = @input.gets
89
+ @tmp.write(line)
90
+ end
91
+
92
+ line
93
+ end
94
+
95
+ def each(&block)
96
+ while line = gets
97
+ yield line
98
+ end
99
+
100
+ self # Rack does not specify what the return value here
101
+ end
102
+
103
+ def rewind
104
+ @tmp.rewind # Rack does not specify what the return value here
105
+ end
106
+
107
+ private
108
+
109
+ # tees off a +length+ chunk of data from the input into the IO
110
+ # backing store as well as returning it. +buf+ must be specified.
111
+ # returns nil if reading from the input returns nil
112
+ def tee(length, buf)
113
+ begin
114
+ if @size
115
+ left = @size - @tmp.stat.size
116
+ 0 == left and return nil
117
+ if length >= left
118
+ @input.readpartial(left, buf) == left and @input = nil
119
+ elsif @input.nil?
120
+ return nil
121
+ else
122
+ @input.readpartial(length, buf)
123
+ end
124
+ else # ChunkedReader#readpartial just raises EOFError when done
125
+ @input.readpartial(length, buf)
126
+ end
127
+ rescue EOFError
128
+ return @input = nil
129
+ end
130
+ @tmp.write(buf)
131
+ buf
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2009 Eric Wong
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+ require 'unicorn'
4
+ require 'unicorn/http11'
5
+
6
+ # Eventually I should integrate this into HttpParser...
7
+ module Unicorn
8
+ class TrailerParser
9
+
10
+ TR_FR = 'a-z-'.freeze
11
+ TR_TO = 'A-Z_'.freeze
12
+
13
+ # initializes HTTP trailer parser with acceptable +trailer+
14
+ def initialize(http_trailer)
15
+ @trailers = http_trailer.split(/\s*,\s*/).inject({}) { |hash, key|
16
+ hash[key.tr(TR_FR, TR_TO)] = true
17
+ hash
18
+ }
19
+ end
20
+
21
+ # Executes our TrailerParser on +data+ and modifies +env+ This will
22
+ # shrink +data+ as it is being consumed. Returns true if it has
23
+ # parsed all trailers, false if not. It raises HttpParserError on
24
+ # parse failure or unknown headers. It has slightly smaller limits
25
+ # than the C-based HTTP parser but should not be an issue in practice
26
+ # since Content-MD5 is probably the only legitimate use for it.
27
+ def execute!(env, data)
28
+ data.size > 0xffff and
29
+ raise HttpParserError, "trailer buffer too large: #{data.size} bytes"
30
+
31
+ begin
32
+ data.sub!(/\A([^\r]+)\r\n/, Z) or return false # need more data
33
+
34
+ key, val = $1.split(/:\s*/, 2)
35
+
36
+ key.size > 256 and
37
+ raise HttpParserError, "trailer key #{key.inspect} is too long"
38
+ val.size > 8192 and
39
+ raise HttpParserError, "trailer value #{val.inspect} is too long"
40
+
41
+ key.tr!(TR_FR, TR_TO)
42
+
43
+ @trailers.delete(key) or
44
+ raise HttpParserError, "unknown trailer: #{key.inspect}"
45
+ env["HTTP_#{key}"] = val
46
+
47
+ @trailers.empty? and return true
48
+ end while true
49
+ end
50
+
51
+ end
52
+ end
data/lib/unicorn/util.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'fcntl'
2
- require 'tmpdir'
3
2
 
4
3
  module Unicorn
5
4
  class Util
@@ -40,22 +39,6 @@ module Unicorn
40
39
  nr
41
40
  end
42
41
 
43
- # creates and returns a new File object. The File is unlinked
44
- # immediately, switched to binary mode, and userspace output
45
- # buffering is disabled
46
- def tmpio
47
- fp = begin
48
- File.open("#{Dir::tmpdir}/#{rand}",
49
- File::RDWR|File::CREAT|File::EXCL, 0600)
50
- rescue Errno::EEXIST
51
- retry
52
- end
53
- File.unlink(fp.path)
54
- fp.binmode
55
- fp.sync = true
56
- fp
57
- end
58
-
59
42
  end
60
43
 
61
44
  end
data/local.mk.sample CHANGED
@@ -38,7 +38,7 @@ publish_doc:
38
38
  # Create gzip variants of the same timestamp as the original so nginx
39
39
  # "gzip_static on" can serve the gzipped versions directly.
40
40
  doc_gz: suf := html js css
41
- doc_gz: globs := $(addprefix doc/*.,$(suf)) $(addprefix doc/*/*.,$(suf))
42
- doc_gz: docs := $(wildcard $(globs))
41
+ doc_gz: docs = $(shell find doc/ -regex '^.*\.\(html\|js\|css\)$$')
43
42
  doc_gz:
44
- for i in $(docs); do gzip < $$i > $$i.gz; touch -r $$i $$i.gz; done
43
+ for i in $(docs); do \
44
+ gzip --rsyncable < $$i > $$i.gz; touch -r $$i $$i.gz; done
@@ -142,18 +142,24 @@ logger Logger.new('#{COMMON_TMP.path}')
142
142
  end
143
143
  end
144
144
  end
145
- resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost`
146
- assert $?.success?
147
- resp = resp.split(/\r?\n/)
148
- grepped = resp.grep(/^sha1: (.{40})/)
149
- assert_equal 1, grepped.size
150
- assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1])
151
-
152
- grepped = resp.grep(/^Content-Type:\s+(.+)/i)
153
- assert_equal 1, grepped.size
154
- assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1]
155
-
156
- assert_equal 1, resp.grep(/^Status:/i).size
145
+
146
+ # fixed in Rack commit 44ed4640f077504a49b7f1cabf8d6ad7a13f6441,
147
+ # no released version of Rails or Rack has this fix
148
+ if RB_V[0] >= 1 && RB_V[1] >= 9
149
+ warn "multipart broken with Rack 1.0.0 and Rails 2.3.2.1 under 1.9"
150
+ else
151
+ resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost`
152
+ assert $?.success?
153
+ resp = resp.split(/\r?\n/)
154
+ grepped = resp.grep(/^sha1: (.{40})/)
155
+ assert_equal 1, grepped.size
156
+ assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1])
157
+
158
+ grepped = resp.grep(/^Content-Type:\s+(.+)/i)
159
+ assert_equal 1, grepped.size
160
+ assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1]
161
+ assert_equal 1, resp.grep(/^Status:/i).size
162
+ end
157
163
 
158
164
  # make sure we can get 403 responses, too
159
165
  uri = URI.parse("http://#@addr:#@port/foo/xpost")
data/test/test_helper.rb CHANGED
@@ -262,3 +262,29 @@ def wait_for_death(pid)
262
262
  end
263
263
  raise "PID:#{pid} never died!"
264
264
  end
265
+
266
+ # executes +cmd+ and chunks its STDOUT
267
+ def chunked_spawn(stdout, *cmd)
268
+ fork {
269
+ crd, cwr = IO.pipe
270
+ crd.binmode
271
+ cwr.binmode
272
+ crd.sync = cwr.sync = true
273
+
274
+ pid = fork {
275
+ STDOUT.reopen(cwr)
276
+ crd.close
277
+ cwr.close
278
+ exec(*cmd)
279
+ }
280
+ cwr.close
281
+ begin
282
+ buf = crd.readpartial(16384)
283
+ stdout.write("#{'%x' % buf.size}\r\n#{buf}")
284
+ rescue EOFError
285
+ stdout.write("0\r\n")
286
+ pid, status = Process.waitpid(pid)
287
+ exit status.exitstatus
288
+ end while true
289
+ }
290
+ end