unicorn 1.1.7 → 2.0.0pre1
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/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
|