unicorn 0.8.4 → 0.9.0

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.
@@ -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