unicorn 1.1.7 → 2.0.0pre1
Sign up to get free protection for your applications and to get access to all the features.
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +14 -5
- data/Rakefile +3 -28
- data/TODO +7 -0
- data/bin/unicorn +9 -13
- data/bin/unicorn_rails +12 -14
- data/examples/big_app_gc.rb +33 -2
- data/ext/unicorn_http/global_variables.h +3 -1
- data/ext/unicorn_http/unicorn_http.rl +15 -6
- data/lib/unicorn.rb +67 -820
- data/lib/unicorn/app/exec_cgi.rb +3 -4
- data/lib/unicorn/configurator.rb +20 -25
- data/lib/unicorn/const.rb +26 -25
- data/lib/unicorn/http_request.rb +64 -57
- data/lib/unicorn/http_response.rb +16 -35
- data/lib/unicorn/http_server.rb +700 -0
- data/lib/unicorn/launcher.rb +4 -3
- data/lib/unicorn/oob_gc.rb +50 -61
- data/lib/unicorn/socket_helper.rb +4 -4
- data/lib/unicorn/tee_input.rb +18 -26
- data/lib/unicorn/tmpio.rb +29 -0
- data/lib/unicorn/util.rb +51 -85
- data/lib/unicorn/worker.rb +40 -0
- data/local.mk.sample +0 -9
- data/script/isolate_for_tests +43 -0
- data/t/GNUmakefile +8 -1
- data/t/t0003-working_directory.sh +0 -5
- data/t/t0010-reap-logging.sh +55 -0
- data/t/t0303-rails3-alt-working_directory_config.ru.sh +0 -5
- data/t/test-rails3.sh +1 -1
- data/test/exec/test_exec.rb +1 -1
- data/test/unit/test_http_parser_ng.rb +11 -0
- data/test/unit/test_request.rb +12 -0
- data/test/unit/test_response.rb +23 -21
- data/test/unit/test_signals.rb +1 -1
- data/test/unit/test_tee_input.rb +21 -19
- data/unicorn.gemspec +3 -2
- metadata +47 -25
- data/t/oob_gc.ru +0 -21
- data/t/oob_gc_path.ru +0 -21
- data/t/t0012-reload-empty-config.sh +0 -82
- data/t/t0018-write-on-close.sh +0 -23
- data/t/t9001-oob_gc.sh +0 -47
- data/t/t9002-oob_gc-path.sh +0 -75
- data/t/write-on-close.ru +0 -11
data/lib/unicorn/launcher.rb
CHANGED
@@ -20,6 +20,7 @@ module Unicorn::Launcher
|
|
20
20
|
# to pickup code changes if the original deployment directory
|
21
21
|
# is a symlink or otherwise got replaced.
|
22
22
|
def self.daemonize!(options)
|
23
|
+
cfg = Unicorn::Configurator
|
23
24
|
$stdin.reopen("/dev/null")
|
24
25
|
|
25
26
|
# We only start a new process group if we're not being reexecuted
|
@@ -52,9 +53,9 @@ module Unicorn::Launcher
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
# $stderr/$stderr can/will be redirected separately in the Unicorn config
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
cfg::DEFAULTS[:stderr_path] ||= "/dev/null"
|
57
|
+
cfg::DEFAULTS[:stdout_path] ||= "/dev/null"
|
58
|
+
cfg::RACKUP[:daemonized] = true
|
58
59
|
end
|
59
60
|
|
60
61
|
end
|
data/lib/unicorn/oob_gc.rb
CHANGED
@@ -1,69 +1,58 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
+
module Unicorn
|
2
3
|
|
3
|
-
#
|
4
|
-
# before attempting to accept more connections.
|
5
|
-
#
|
6
|
-
# This shouldn't hurt overall performance as long as the server cluster
|
7
|
-
# is at <50% CPU capacity, and improves the performance of most memory
|
8
|
-
# intensive requests. This serves to improve _client-visible_
|
9
|
-
# performance (possibly at the cost of overall performance).
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# the client never sees the extra GC hit it.
|
18
|
-
#
|
19
|
-
# This middleware is _only_ effective for applications that use a lot
|
20
|
-
# of memory, and will hurt simpler apps/endpoints that can process
|
21
|
-
# multiple requests before incurring GC.
|
22
|
-
#
|
23
|
-
# This middleware is only designed to work with unicorn, as it harms
|
24
|
-
# performance with keepalive-enabled servers.
|
25
|
-
#
|
26
|
-
# Example (in config.ru):
|
27
|
-
#
|
28
|
-
# require 'unicorn/oob_gc'
|
29
|
-
#
|
30
|
-
# # GC ever two requests that hit /expensive/foo or /more_expensive/foo
|
31
|
-
# # in your app. By default, this will GC once every 5 requests
|
32
|
-
# # for all endpoints in your app
|
33
|
-
# use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)}
|
34
|
-
#
|
35
|
-
# Feedback from users of early implementations of this module:
|
36
|
-
# * http://comments.gmane.org/gmane.comp.lang.ruby.unicorn.general/486
|
37
|
-
# * http://article.gmane.org/gmane.comp.lang.ruby.unicorn.general/596
|
38
|
-
module Unicorn::OobGC
|
39
|
-
|
40
|
-
# this pretends to be Rack middleware because it used to be
|
41
|
-
# But we need to hook into unicorn internals so we need to close
|
42
|
-
# the socket before clearing the request env.
|
4
|
+
# Run GC after every request, after closing the client socket and
|
5
|
+
# before attempting to accept more connections.
|
6
|
+
#
|
7
|
+
# This shouldn't hurt overall performance as long as the server cluster
|
8
|
+
# is at <50% CPU capacity, and improves the performance of most memory
|
9
|
+
# intensive requests. This serves to improve _client-visible_
|
10
|
+
# performance (possibly at the cost of overall performance).
|
11
|
+
#
|
12
|
+
# We'll call GC after each request is been written out to the socket, so
|
13
|
+
# the client never sees the extra GC hit it.
|
14
|
+
#
|
15
|
+
# This middleware is _only_ effective for applications that use a lot
|
16
|
+
# of memory, and will hurt simpler apps/endpoints that can process
|
17
|
+
# multiple requests before incurring GC.
|
43
18
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
19
|
+
# This middleware is only designed to work with Unicorn, as it harms
|
20
|
+
# keepalive performance.
|
21
|
+
#
|
22
|
+
# Example (in config.ru):
|
23
|
+
#
|
24
|
+
# require 'unicorn/oob_gc'
|
25
|
+
#
|
26
|
+
# # GC ever two requests that hit /expensive/foo or /more_expensive/foo
|
27
|
+
# # in your app. By default, this will GC once every 5 requests
|
28
|
+
# # for all endpoints in your app
|
29
|
+
# use Unicorn::OobGC, 2, %r{\A/(?:expensive/foo|more_expensive/foo)}
|
30
|
+
class OobGC < Struct.new(:app, :interval, :path, :nr, :env, :body)
|
31
|
+
|
32
|
+
def initialize(app, interval = 5, path = %r{\A/})
|
33
|
+
super(app, interval, path, interval)
|
53
34
|
end
|
54
|
-
app # pretend to be Rack middleware since it was in the past
|
55
|
-
end
|
56
35
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
GC.start
|
36
|
+
def call(env)
|
37
|
+
status, headers, self.body = app.call(self.env = env)
|
38
|
+
[ status, headers, self ]
|
39
|
+
end
|
40
|
+
|
41
|
+
def each(&block)
|
42
|
+
body.each(&block)
|
65
43
|
end
|
66
|
-
end
|
67
44
|
|
68
|
-
|
45
|
+
# in Unicorn, this is closed _after_ the client socket
|
46
|
+
def close
|
47
|
+
body.close if body.respond_to?(:close)
|
48
|
+
|
49
|
+
if path =~ env['PATH_INFO'] && ((self.nr -= 1) <= 0)
|
50
|
+
self.nr = interval
|
51
|
+
self.body = nil
|
52
|
+
env.clear
|
53
|
+
GC.start
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
69
58
|
end
|
@@ -126,12 +126,12 @@ module Unicorn
|
|
126
126
|
end
|
127
127
|
old_umask = File.umask(opt[:umask] || 0)
|
128
128
|
begin
|
129
|
-
UNIXServer.new(address)
|
129
|
+
Kgio::UNIXServer.new(address)
|
130
130
|
ensure
|
131
131
|
File.umask(old_umask)
|
132
132
|
end
|
133
133
|
elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/
|
134
|
-
TCPServer.new($1, $2.to_i)
|
134
|
+
Kgio::TCPServer.new($1, $2.to_i)
|
135
135
|
else
|
136
136
|
raise ArgumentError, "Don't know how to bind: #{address}"
|
137
137
|
end
|
@@ -166,9 +166,9 @@ module Unicorn
|
|
166
166
|
def server_cast(sock)
|
167
167
|
begin
|
168
168
|
Socket.unpack_sockaddr_in(sock.getsockname)
|
169
|
-
TCPServer.for_fd(sock.fileno)
|
169
|
+
Kgio::TCPServer.for_fd(sock.fileno)
|
170
170
|
rescue ArgumentError
|
171
|
-
UNIXServer.for_fd(sock.fileno)
|
171
|
+
Kgio::UNIXServer.for_fd(sock.fileno)
|
172
172
|
end
|
173
173
|
end
|
174
174
|
|
data/lib/unicorn/tee_input.rb
CHANGED
@@ -25,11 +25,14 @@ class Unicorn::TeeInput < Struct.new(:socket, :req, :parser,
|
|
25
25
|
|
26
26
|
# Initializes a new TeeInput object. You normally do not have to call
|
27
27
|
# this unless you are writing an HTTP server.
|
28
|
-
def initialize(
|
29
|
-
|
28
|
+
def initialize(socket, request)
|
29
|
+
self.socket = socket
|
30
|
+
self.req = request.env
|
31
|
+
self.parser = request.parser
|
32
|
+
self.buf = request.buf
|
30
33
|
self.len = parser.content_length
|
31
34
|
self.tmp = len && len < @@client_body_buffer_size ?
|
32
|
-
StringIO.new("") : Unicorn::
|
35
|
+
StringIO.new("") : Unicorn::TmpIO.new
|
33
36
|
self.buf2 = ""
|
34
37
|
if buf.size > 0
|
35
38
|
parser.filter_body(buf2, buf) and finalize_input
|
@@ -168,44 +171,25 @@ class Unicorn::TeeInput < Struct.new(:socket, :req, :parser,
|
|
168
171
|
|
169
172
|
private
|
170
173
|
|
171
|
-
def client_error(e)
|
172
|
-
case e
|
173
|
-
when EOFError
|
174
|
-
# in case client only did a premature shutdown(SHUT_WR)
|
175
|
-
# we do support clients that shutdown(SHUT_WR) after the
|
176
|
-
# _entire_ request has been sent, and those will not have
|
177
|
-
# raised EOFError on us.
|
178
|
-
socket.close if socket
|
179
|
-
raise Unicorn::ClientShutdown, "bytes_read=#{tmp.size}", []
|
180
|
-
when Unicorn::HttpParserError
|
181
|
-
e.set_backtrace([])
|
182
|
-
end
|
183
|
-
raise e
|
184
|
-
end
|
185
|
-
|
186
174
|
# tees off a +length+ chunk of data from the input into the IO
|
187
175
|
# backing store as well as returning it. +dst+ must be specified.
|
188
176
|
# returns nil if reading from the input returns nil
|
189
177
|
def tee(length, dst)
|
190
178
|
unless parser.body_eof?
|
191
|
-
|
179
|
+
r = socket.kgio_read(length, buf) or eof!
|
180
|
+
unless parser.filter_body(dst, r)
|
192
181
|
tmp.write(dst)
|
193
182
|
tmp.seek(0, IO::SEEK_END) # workaround FreeBSD/OSX + MRI 1.8.x bug
|
194
183
|
return dst
|
195
184
|
end
|
196
185
|
end
|
197
186
|
finalize_input
|
198
|
-
rescue => e
|
199
|
-
client_error(e)
|
200
187
|
end
|
201
188
|
|
202
189
|
def finalize_input
|
203
190
|
while parser.trailers(req, buf).nil?
|
204
|
-
|
205
|
-
|
206
|
-
# initialize we never get any chance to enter the app so the
|
207
|
-
# EOFError will just get trapped by Unicorn and not the Rack app
|
208
|
-
buf << socket.readpartial(@@io_chunk_size)
|
191
|
+
r = socket.kgio_read(@@io_chunk_size) or eof!
|
192
|
+
buf << r
|
209
193
|
end
|
210
194
|
self.socket = nil
|
211
195
|
end
|
@@ -229,4 +213,12 @@ private
|
|
229
213
|
dst
|
230
214
|
end
|
231
215
|
|
216
|
+
def eof!
|
217
|
+
# in case client only did a premature shutdown(SHUT_WR)
|
218
|
+
# we do support clients that shutdown(SHUT_WR) after the
|
219
|
+
# _entire_ request has been sent, and those will not have
|
220
|
+
# raised EOFError on us.
|
221
|
+
socket.close if socket
|
222
|
+
raise Unicorn::ClientShutdown, "bytes_read=#{tmp.size}", []
|
223
|
+
end
|
232
224
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :stopdoc:
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
# some versions of Ruby had a broken Tempfile which didn't work
|
6
|
+
# well with unlinked files. This one is much shorter, easier
|
7
|
+
# to understand, and slightly faster.
|
8
|
+
class Unicorn::TmpIO < File
|
9
|
+
|
10
|
+
# creates and returns a new File object. The File is unlinked
|
11
|
+
# immediately, switched to binary mode, and userspace output
|
12
|
+
# buffering is disabled
|
13
|
+
def self.new
|
14
|
+
fp = begin
|
15
|
+
super("#{Dir::tmpdir}/#{rand}", RDWR|CREAT|EXCL, 0600)
|
16
|
+
rescue Errno::EEXIST
|
17
|
+
retry
|
18
|
+
end
|
19
|
+
unlink(fp.path)
|
20
|
+
fp.binmode
|
21
|
+
fp.sync = true
|
22
|
+
fp
|
23
|
+
end
|
24
|
+
|
25
|
+
# for easier env["rack.input"] compatibility with Rack <= 1.1
|
26
|
+
def size
|
27
|
+
stat.size
|
28
|
+
end
|
29
|
+
end
|
data/lib/unicorn/util.rb
CHANGED
@@ -1,101 +1,67 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Unicorn::Util
|
4
|
+
|
5
|
+
# :stopdoc:
|
6
|
+
def self.is_log?(fp)
|
7
|
+
append_flags = File::WRONLY | File::APPEND
|
8
|
+
|
9
|
+
! fp.closed? &&
|
10
|
+
fp.sync &&
|
11
|
+
fp.path[0] == ?/ &&
|
12
|
+
(fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
|
13
|
+
rescue IOError, Errno::EBADF
|
14
|
+
false
|
15
|
+
end
|
9
16
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
stat.size
|
17
|
+
def self.chown_logs(uid, gid)
|
18
|
+
ObjectSpace.each_object(File) do |fp|
|
19
|
+
fp.chown(uid, gid) if is_log?(fp)
|
14
20
|
end
|
15
21
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
# :startdoc:
|
23
|
+
|
24
|
+
# This reopens ALL logfiles in the process that have been rotated
|
25
|
+
# using logrotate(8) (without copytruncate) or similar tools.
|
26
|
+
# A +File+ object is considered for reopening if it is:
|
27
|
+
# 1) opened with the O_APPEND and O_WRONLY flags
|
28
|
+
# 2) opened with an absolute path (starts with "/")
|
29
|
+
# 3) the current open file handle does not match its original open path
|
30
|
+
# 4) unbuffered (as far as userspace buffering goes, not O_SYNC)
|
31
|
+
# Returns the number of files reopened
|
32
|
+
def self.reopen_logs
|
33
|
+
to_reopen = []
|
34
|
+
nr = 0
|
35
|
+
ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
|
36
|
+
|
37
|
+
to_reopen.each do |fp|
|
38
|
+
orig_st = begin
|
39
|
+
fp.stat
|
40
|
+
rescue IOError, Errno::EBADF
|
41
|
+
next
|
29
42
|
end
|
30
43
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
44
|
+
begin
|
45
|
+
b = File.stat(fp.path)
|
46
|
+
next if orig_st.ino == b.ino && orig_st.dev == b.dev
|
47
|
+
rescue Errno::ENOENT
|
35
48
|
end
|
36
49
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# 2) opened with an absolute path (starts with "/")
|
42
|
-
# 3) the current open file handle does not match its original open path
|
43
|
-
# 4) unbuffered (as far as userspace buffering goes, not O_SYNC)
|
44
|
-
# Returns the number of files reopened
|
45
|
-
def reopen_logs
|
46
|
-
to_reopen = []
|
47
|
-
nr = 0
|
48
|
-
ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
|
49
|
-
|
50
|
-
to_reopen.each do |fp|
|
51
|
-
orig_st = begin
|
52
|
-
fp.stat
|
53
|
-
rescue IOError, Errno::EBADF
|
54
|
-
next
|
55
|
-
end
|
56
|
-
|
57
|
-
begin
|
58
|
-
b = File.stat(fp.path)
|
59
|
-
next if orig_st.ino == b.ino && orig_st.dev == b.dev
|
60
|
-
rescue Errno::ENOENT
|
61
|
-
end
|
62
|
-
|
63
|
-
begin
|
64
|
-
File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
|
65
|
-
fp.sync = true
|
66
|
-
new_st = fp.stat
|
67
|
-
|
68
|
-
# this should only happen in the master:
|
69
|
-
if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
|
70
|
-
fp.chown(orig_st.uid, orig_st.gid)
|
71
|
-
end
|
50
|
+
begin
|
51
|
+
File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
|
52
|
+
fp.sync = true
|
53
|
+
new_st = fp.stat
|
72
54
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
55
|
+
# this should only happen in the master:
|
56
|
+
if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
|
57
|
+
fp.chown(orig_st.uid, orig_st.gid)
|
77
58
|
end
|
78
59
|
|
79
|
-
nr
|
60
|
+
nr += 1
|
61
|
+
rescue IOError, Errno::EBADF
|
62
|
+
# not much we can do...
|
80
63
|
end
|
81
|
-
|
82
|
-
# creates and returns a new File object. The File is unlinked
|
83
|
-
# immediately, switched to binary mode, and userspace output
|
84
|
-
# buffering is disabled
|
85
|
-
def tmpio
|
86
|
-
fp = begin
|
87
|
-
TmpIO.open("#{Dir::tmpdir}/#{rand}",
|
88
|
-
File::RDWR|File::CREAT|File::EXCL, 0600)
|
89
|
-
rescue Errno::EEXIST
|
90
|
-
retry
|
91
|
-
end
|
92
|
-
File.unlink(fp.path)
|
93
|
-
fp.binmode
|
94
|
-
fp.sync = true
|
95
|
-
fp
|
96
|
-
end
|
97
|
-
|
98
64
|
end
|
99
|
-
|
65
|
+
nr
|
100
66
|
end
|
101
67
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
# This class and its members can be considered a stable interface
|
4
|
+
# and will not change in a backwards-incompatible fashion between
|
5
|
+
# releases of Unicorn. You may need to access it in the
|
6
|
+
# before_fork/after_fork hooks. See the Unicorn::Configurator RDoc
|
7
|
+
# for examples.
|
8
|
+
class Unicorn::Worker < Struct.new(:nr, :tmp, :switched)
|
9
|
+
|
10
|
+
# worker objects may be compared to just plain numbers
|
11
|
+
def ==(other_nr)
|
12
|
+
self.nr == other_nr
|
13
|
+
end
|
14
|
+
|
15
|
+
# Changes the worker process to the specified +user+ and +group+
|
16
|
+
# This is only intended to be called from within the worker
|
17
|
+
# process from the +after_fork+ hook. This should be called in
|
18
|
+
# the +after_fork+ hook after any priviledged functions need to be
|
19
|
+
# run (e.g. to set per-worker CPU affinity, niceness, etc)
|
20
|
+
#
|
21
|
+
# Any and all errors raised within this method will be propagated
|
22
|
+
# directly back to the caller (usually the +after_fork+ hook.
|
23
|
+
# These errors commonly include ArgumentError for specifying an
|
24
|
+
# invalid user/group and Errno::EPERM for insufficient priviledges
|
25
|
+
def user(user, group = nil)
|
26
|
+
# we do not protect the caller, checking Process.euid == 0 is
|
27
|
+
# insufficient because modern systems have fine-grained
|
28
|
+
# capabilities. Let the caller handle any and all errors.
|
29
|
+
uid = Etc.getpwnam(user).uid
|
30
|
+
gid = Etc.getgrnam(group).gid if group
|
31
|
+
Unicorn::Util.chown_logs(uid, gid)
|
32
|
+
tmp.chown(uid, gid)
|
33
|
+
if gid && Process.egid != gid
|
34
|
+
Process.initgroups(user, gid)
|
35
|
+
Process::GID.change_privilege(gid)
|
36
|
+
end
|
37
|
+
Process.euid != uid and Process::UID.change_privilege(uid)
|
38
|
+
self.switched = true
|
39
|
+
end
|
40
|
+
end
|