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/GIT-VERSION-GEN
CHANGED
data/GNUmakefile
CHANGED
@@ -24,6 +24,15 @@ endif
|
|
24
24
|
|
25
25
|
RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
|
26
26
|
|
27
|
+
isolate_libs := tmp/isolate/.$(RUBY_ENGINE)-$(RUBY_VERSION).libs
|
28
|
+
MYLIBS = $(RUBYLIB):$(shell cat $(isolate_libs) 2>/dev/null || \
|
29
|
+
(test -f ./script/isolate_for_tests && \
|
30
|
+
$(RUBY) ./script/isolate_for_tests >/dev/null && \
|
31
|
+
cat $(isolate_libs) 2>/dev/null))
|
32
|
+
|
33
|
+
echo:
|
34
|
+
@echo $(MYLIBS)
|
35
|
+
|
27
36
|
# dunno how to implement this as concisely in Ruby, and hell, I love awk
|
28
37
|
awk_slow := awk '/def test_/{print FILENAME"--"$$2".n"}' 2>/dev/null
|
29
38
|
|
@@ -117,14 +126,14 @@ run_test = $(quiet_pre) \
|
|
117
126
|
%.n: arg = $(subst .n,,$(subst --, -n ,$@))
|
118
127
|
%.n: t = $(subst .n,$(log_suffix),$@)
|
119
128
|
%.n: export PATH := $(test_prefix)/bin:$(PATH)
|
120
|
-
%.n: export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(
|
129
|
+
%.n: export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(MYLIBS)
|
121
130
|
%.n: $(test_prefix)/.stamp
|
122
131
|
$(run_test)
|
123
132
|
|
124
133
|
$(T): arg = $@
|
125
134
|
$(T): t = $(subst .rb,$(log_suffix),$@)
|
126
135
|
$(T): export PATH := $(test_prefix)/bin:$(PATH)
|
127
|
-
$(T): export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(
|
136
|
+
$(T): export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(MYLIBS)
|
128
137
|
$(T): $(test_prefix)/.stamp
|
129
138
|
$(run_test)
|
130
139
|
|
@@ -169,7 +178,7 @@ NEWS: GIT-VERSION-FILE .manifest
|
|
169
178
|
$(RAKE) -s news_rdoc > $@+
|
170
179
|
mv $@+ $@
|
171
180
|
|
172
|
-
SINCE = 1.
|
181
|
+
SINCE = 1.1.4
|
173
182
|
ChangeLog: LOG_VERSION = \
|
174
183
|
$(shell git rev-parse -q "$(GIT_VERSION)" >/dev/null 2>&1 && \
|
175
184
|
echo $(GIT_VERSION) || git describe)
|
@@ -189,7 +198,7 @@ atom = <link rel="alternate" title="Atom feed" href="$(1)" \
|
|
189
198
|
doc: .document $(ext)/unicorn_http.c NEWS ChangeLog
|
190
199
|
for i in $(man1_rdoc); do echo > $$i; done
|
191
200
|
find bin lib -type f -name '*.rbc' -exec rm -f '{}' ';'
|
192
|
-
rdoc -t "$(shell sed -ne '1s/^= //p' README)"
|
201
|
+
rdoc -a -t "$(shell sed -ne '1s/^= //p' README)"
|
193
202
|
install -m644 COPYING doc/COPYING
|
194
203
|
install -m644 $(shell grep '^[A-Z]' .document) doc/
|
195
204
|
$(MAKE) -C Documentation install-html install-man
|
@@ -251,7 +260,7 @@ $(T_r).%.r: rv = $(subst .r,,$(subst $(T_r).,,$@))
|
|
251
260
|
$(T_r).%.r: extra = ' 'v$(rv)
|
252
261
|
$(T_r).%.r: arg = $(T_r)
|
253
262
|
$(T_r).%.r: export PATH := $(test_prefix)/bin:$(PATH)
|
254
|
-
$(T_r).%.r: export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(
|
263
|
+
$(T_r).%.r: export RUBYLIB := $(test_prefix):$(test_prefix)/lib:$(MYLIBS)
|
255
264
|
$(T_r).%.r: export UNICORN_RAILS_TEST_VERSION = $(rv)
|
256
265
|
$(T_r).%.r: export RAILS_GIT_REPO = $(CURDIR)/$(rails_git)
|
257
266
|
$(T_r).%.r: $(test_prefix)/.stamp $(rails_git)/info/v2.3.8-stamp
|
data/Rakefile
CHANGED
@@ -15,7 +15,7 @@ def tags
|
|
15
15
|
timefmt = '%Y-%m-%dT%H:%M:%SZ'
|
16
16
|
@tags ||= `git tag -l`.split(/\n/).map do |tag|
|
17
17
|
next if tag == "v0.0.0"
|
18
|
-
if %r{\Av[\d\.]
|
18
|
+
if %r{\Av[\d\.]+\z} =~ tag
|
19
19
|
header, subject, body = `git cat-file tag #{tag}`.split(/\n\n/, 3)
|
20
20
|
header = header.split(/\n/)
|
21
21
|
tagger = header.grep(/\Atagger /).first
|
@@ -163,11 +163,12 @@ task :fm_update do
|
|
163
163
|
req = {
|
164
164
|
"auth_code" => api_token,
|
165
165
|
"release" => {
|
166
|
-
"tag_list" => "
|
166
|
+
"tag_list" => "Experimental",
|
167
167
|
"version" => version,
|
168
168
|
"changelog" => changelog,
|
169
169
|
},
|
170
170
|
}.to_json
|
171
|
+
|
171
172
|
if ! changelog.strip.empty? && version =~ %r{\A[\d\.]+\d+\z}
|
172
173
|
Net::HTTP.start(uri.host, uri.port) do |http|
|
173
174
|
p http.post(uri.path, req, {'Content-Type'=>'application/json'})
|
@@ -193,29 +194,3 @@ begin
|
|
193
194
|
end
|
194
195
|
rescue LoadError
|
195
196
|
end
|
196
|
-
|
197
|
-
task :isolate do
|
198
|
-
require 'isolate'
|
199
|
-
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
|
200
|
-
opts = {
|
201
|
-
:system => false,
|
202
|
-
:path => "tmp/isolate/#{ruby_engine}-#{RUBY_VERSION}",
|
203
|
-
:multiruby => false, # we want "1.8.7" instead of "1.8"
|
204
|
-
}
|
205
|
-
fp = File.open(__FILE__, "rb")
|
206
|
-
fp.flock(File::LOCK_EX)
|
207
|
-
|
208
|
-
# C extensions aren't binary-compatible across Ruby versions
|
209
|
-
pid = fork { Isolate.now!(opts) { gem 'sqlite3-ruby', '1.2.5' } }
|
210
|
-
_, status = Process.waitpid2(pid)
|
211
|
-
status.success? or abort status.inspect
|
212
|
-
|
213
|
-
# pure Ruby gems can be shared across all Rubies
|
214
|
-
%w(3.0.0).each do |rails_ver|
|
215
|
-
opts[:path] = "tmp/isolate/rails-#{rails_ver}"
|
216
|
-
pid = fork { Isolate.now!(opts) { gem 'rails', rails_ver } }
|
217
|
-
_, status = Process.waitpid2(pid)
|
218
|
-
status.success? or abort status.inspect
|
219
|
-
end
|
220
|
-
fp.flock(File::LOCK_UN)
|
221
|
-
end
|
data/TODO
CHANGED
@@ -3,3 +3,10 @@
|
|
3
3
|
* performance validation (esp. TeeInput)
|
4
4
|
|
5
5
|
* improve test suite
|
6
|
+
|
7
|
+
* scalability to >= 1024 worker processes for crazy NUMA systems
|
8
|
+
|
9
|
+
* Rack 2.x support (when Rack 2.x exists)
|
10
|
+
|
11
|
+
* allow disabling "rack.input" rewindability for performance
|
12
|
+
(but violate the Rack 1.x SPEC)
|
data/bin/unicorn
CHANGED
@@ -4,16 +4,13 @@ require 'unicorn/launcher'
|
|
4
4
|
require 'optparse'
|
5
5
|
|
6
6
|
ENV["RACK_ENV"] ||= "development"
|
7
|
-
|
8
|
-
options =
|
9
|
-
host, port = Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT
|
10
|
-
set_listener = false
|
7
|
+
rackup_opts = Unicorn::Configurator::RACKUP
|
8
|
+
options = rackup_opts[:options]
|
11
9
|
|
12
10
|
opts = OptionParser.new("", 24, ' ') do |opts|
|
13
11
|
cmd = File.basename($0)
|
14
12
|
opts.banner = "Usage: #{cmd} " \
|
15
13
|
"[ruby options] [#{cmd} options] [rackup config file]"
|
16
|
-
|
17
14
|
opts.separator "Ruby options:"
|
18
15
|
|
19
16
|
lineno = 1
|
@@ -46,14 +43,14 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
46
43
|
|
47
44
|
opts.on("-o", "--host HOST",
|
48
45
|
"listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h|
|
49
|
-
host = h
|
50
|
-
set_listener = true
|
46
|
+
rackup_opts[:host] = h
|
47
|
+
rackup_opts[:set_listener] = true
|
51
48
|
end
|
52
49
|
|
53
50
|
opts.on("-p", "--port PORT",
|
54
51
|
"use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |p|
|
55
|
-
port = p.to_i
|
56
|
-
set_listener = true
|
52
|
+
rackup_opts[:port] = p.to_i
|
53
|
+
rackup_opts[:set_listener] = true
|
57
54
|
end
|
58
55
|
|
59
56
|
opts.on("-E", "--env ENVIRONMENT",
|
@@ -62,7 +59,7 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
62
59
|
end
|
63
60
|
|
64
61
|
opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
|
65
|
-
daemonize = d
|
62
|
+
rackup_opts[:daemonize] = !!d
|
66
63
|
end
|
67
64
|
|
68
65
|
opts.on("-P", "--pid FILE", "DEPRECATED") do |f|
|
@@ -109,16 +106,15 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
109
106
|
end
|
110
107
|
|
111
108
|
app = Unicorn.builder(ARGV[0] || 'config.ru', opts)
|
112
|
-
options[:listeners] << "#{host}:#{port}" if set_listener
|
113
109
|
|
114
110
|
if $DEBUG
|
115
111
|
require 'pp'
|
116
112
|
pp({
|
117
113
|
:unicorn_options => options,
|
118
114
|
:app => app,
|
119
|
-
:daemonize => daemonize,
|
115
|
+
:daemonize => rackup_opts[:daemonize],
|
120
116
|
})
|
121
117
|
end
|
122
118
|
|
123
|
-
Unicorn::Launcher.daemonize!(options) if daemonize
|
119
|
+
Unicorn::Launcher.daemonize!(options) if rackup_opts[:daemonize]
|
124
120
|
Unicorn.run(app, options)
|
data/bin/unicorn_rails
CHANGED
@@ -4,11 +4,9 @@ require 'unicorn/launcher'
|
|
4
4
|
require 'optparse'
|
5
5
|
require 'fileutils'
|
6
6
|
|
7
|
-
daemonize = false
|
8
|
-
options = { :listeners => [] }
|
9
|
-
host, port = Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT
|
10
|
-
set_listener = false
|
11
7
|
ENV['RAILS_ENV'] ||= "development"
|
8
|
+
rackup_opts = Unicorn::Configurator::RACKUP
|
9
|
+
options = rackup_opts[:options]
|
12
10
|
|
13
11
|
opts = OptionParser.new("", 24, ' ') do |opts|
|
14
12
|
cmd = File.basename($0)
|
@@ -46,13 +44,14 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
46
44
|
|
47
45
|
opts.on("-o", "--host HOST",
|
48
46
|
"listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h|
|
49
|
-
host = h
|
50
|
-
set_listener = true
|
47
|
+
rackup_opts[:host] = h
|
48
|
+
rackup_opts[:set_listener] = true
|
51
49
|
end
|
52
50
|
|
53
|
-
opts.on("-p", "--port PORT",
|
54
|
-
|
55
|
-
|
51
|
+
opts.on("-p", "--port PORT",
|
52
|
+
"use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |p|
|
53
|
+
rackup_opts[:port] = p.to_i
|
54
|
+
rackup_opts[:set_listener] = true
|
56
55
|
end
|
57
56
|
|
58
57
|
opts.on("-E", "--env RAILS_ENV",
|
@@ -61,7 +60,7 @@ opts = OptionParser.new("", 24, ' ') do |opts|
|
|
61
60
|
end
|
62
61
|
|
63
62
|
opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
|
64
|
-
daemonize = d
|
63
|
+
rackup_opts[:daemonize] = !!d
|
65
64
|
end
|
66
65
|
|
67
66
|
# Unicorn-specific stuff
|
@@ -186,15 +185,14 @@ def rails_builder(ru, opts, daemonize)
|
|
186
185
|
end
|
187
186
|
end
|
188
187
|
|
189
|
-
app = rails_builder(ARGV[0], opts, daemonize)
|
190
|
-
options[:listeners] << "#{host}:#{port}" if set_listener
|
188
|
+
app = rails_builder(ARGV[0], opts, rackup_opts[:daemonize])
|
191
189
|
|
192
190
|
if $DEBUG
|
193
191
|
require 'pp'
|
194
192
|
pp({
|
195
193
|
:unicorn_options => options,
|
196
194
|
:app => app,
|
197
|
-
:daemonize => daemonize,
|
195
|
+
:daemonize => rackup_opts[:daemonize],
|
198
196
|
})
|
199
197
|
end
|
200
198
|
|
@@ -203,7 +201,7 @@ options[:after_reload] = lambda do
|
|
203
201
|
FileUtils.mkdir_p(%w(cache pids sessions sockets).map! { |d| "tmp/#{d}" })
|
204
202
|
end
|
205
203
|
|
206
|
-
if daemonize
|
204
|
+
if rackup_opts[:daemonize]
|
207
205
|
options[:pid] = "tmp/pids/unicorn.pid"
|
208
206
|
Unicorn::Launcher.daemonize!(options)
|
209
207
|
end
|
data/examples/big_app_gc.rb
CHANGED
@@ -1,2 +1,33 @@
|
|
1
|
-
#
|
2
|
-
#
|
1
|
+
# Run GC after every request, before attempting to accept more connections.
|
2
|
+
#
|
3
|
+
# You could customize this patch to read REQ["PATH_INFO"] and only
|
4
|
+
# call GC.start after expensive requests.
|
5
|
+
#
|
6
|
+
# We could have this wrap the response body.close as middleware, but the
|
7
|
+
# scannable stack is would still be bigger than it would be here.
|
8
|
+
#
|
9
|
+
# This shouldn't hurt overall performance as long as the server cluster
|
10
|
+
# is at <=50% CPU capacity, and improves the performance of most memory
|
11
|
+
# intensive requests. This serves to improve _client-visible_
|
12
|
+
# performance (possibly at the cost of overall performance).
|
13
|
+
#
|
14
|
+
# We'll call GC after each request is been written out to the socket, so
|
15
|
+
# the client never sees the extra GC hit it. It's ideal to call the GC
|
16
|
+
# inside the HTTP server (vs middleware or hooks) since the stack is
|
17
|
+
# smaller at this point, so the GC will both be faster and more
|
18
|
+
# effective at releasing unused memory.
|
19
|
+
#
|
20
|
+
# This monkey patch is _only_ effective for applications that use a lot
|
21
|
+
# of memory, and will hurt simpler apps/endpoints that can process
|
22
|
+
# multiple requests before incurring GC.
|
23
|
+
|
24
|
+
class Unicorn::HttpServer
|
25
|
+
REQ = Unicorn::HttpRequest::REQ
|
26
|
+
alias _process_client process_client
|
27
|
+
undef_method :process_client
|
28
|
+
def process_client(client)
|
29
|
+
_process_client(client)
|
30
|
+
REQ.clear
|
31
|
+
GC.start
|
32
|
+
end
|
33
|
+
end if defined?(Unicorn)
|
@@ -35,13 +35,15 @@ static VALUE g_HEAD;
|
|
35
35
|
static const char * const MAX_##N##_LENGTH_ERR = \
|
36
36
|
"HTTP element " # N " is longer than the " # length " allowed length."
|
37
37
|
|
38
|
+
NORETURN(static void parser_error(const char *));
|
39
|
+
|
38
40
|
/**
|
39
41
|
* Validates the max length of given input and throws an HttpParserError
|
40
42
|
* exception if over.
|
41
43
|
*/
|
42
44
|
#define VALIDATE_MAX_LENGTH(len, N) do { \
|
43
45
|
if (len > MAX_##N##_LENGTH) \
|
44
|
-
|
46
|
+
parser_error(MAX_##N##_LENGTH_ERR); \
|
45
47
|
} while (0)
|
46
48
|
|
47
49
|
/** Defines global strings in the init method. */
|
@@ -48,6 +48,15 @@ struct http_parser {
|
|
48
48
|
|
49
49
|
static void finalize_header(struct http_parser *hp, VALUE req);
|
50
50
|
|
51
|
+
static void parser_error(const char *msg)
|
52
|
+
{
|
53
|
+
VALUE exc = rb_exc_new2(eHttpParserError, msg);
|
54
|
+
VALUE bt = rb_ary_new();
|
55
|
+
|
56
|
+
rb_funcall(exc, rb_intern("set_backtrace"), 1, bt);
|
57
|
+
rb_exc_raise(exc);
|
58
|
+
}
|
59
|
+
|
51
60
|
#define REMAINING (unsigned long)(pe - p)
|
52
61
|
#define LEN(AT, FPC) (FPC - buffer - hp->AT)
|
53
62
|
#define MARK(M,FPC) (hp->M = (FPC) - buffer)
|
@@ -132,7 +141,7 @@ http_version(struct http_parser *hp, VALUE req, const char *ptr, size_t len)
|
|
132
141
|
static inline void hp_invalid_if_trailer(struct http_parser *hp)
|
133
142
|
{
|
134
143
|
if (HP_FL_TEST(hp, INTRAILER))
|
135
|
-
|
144
|
+
parser_error("invalid Trailer");
|
136
145
|
}
|
137
146
|
|
138
147
|
static void write_cont_value(struct http_parser *hp,
|
@@ -141,7 +150,7 @@ static void write_cont_value(struct http_parser *hp,
|
|
141
150
|
char *vptr;
|
142
151
|
|
143
152
|
if (hp->cont == Qfalse)
|
144
|
-
|
153
|
+
parser_error("invalid continuation line");
|
145
154
|
if (NIL_P(hp->cont))
|
146
155
|
return; /* we're ignoring this header (probably Host:) */
|
147
156
|
|
@@ -192,7 +201,7 @@ static void write_value(VALUE req, struct http_parser *hp,
|
|
192
201
|
} else if (f == g_content_length) {
|
193
202
|
hp->len.content = parse_length(RSTRING_PTR(v), RSTRING_LEN(v));
|
194
203
|
if (hp->len.content < 0)
|
195
|
-
|
204
|
+
parser_error("invalid Content-Length");
|
196
205
|
HP_FL_SET(hp, HASBODY);
|
197
206
|
hp_invalid_if_trailer(hp);
|
198
207
|
} else if (f == g_http_transfer_encoding) {
|
@@ -285,7 +294,7 @@ static void write_value(VALUE req, struct http_parser *hp,
|
|
285
294
|
action add_to_chunk_size {
|
286
295
|
hp->len.chunk = step_incr(hp->len.chunk, fc, 16);
|
287
296
|
if (hp->len.chunk < 0)
|
288
|
-
|
297
|
+
parser_error("invalid chunk size");
|
289
298
|
}
|
290
299
|
action header_done {
|
291
300
|
finalize_header(hp, req);
|
@@ -550,7 +559,7 @@ static VALUE HttpParser_headers(VALUE self, VALUE req, VALUE data)
|
|
550
559
|
}
|
551
560
|
|
552
561
|
if (hp->cs == http_parser_error)
|
553
|
-
|
562
|
+
parser_error("Invalid HTTP format, parsing fails.");
|
554
563
|
|
555
564
|
return Qnil;
|
556
565
|
}
|
@@ -643,7 +652,7 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE buf, VALUE data)
|
|
643
652
|
hp->s.dest_offset = 0;
|
644
653
|
http_parser_execute(hp, buf, dptr, dlen);
|
645
654
|
if (hp->cs == http_parser_error)
|
646
|
-
|
655
|
+
parser_error("Invalid HTTP format, parsing fails.");
|
647
656
|
|
648
657
|
assert(hp->s.dest_offset <= hp->offset &&
|
649
658
|
"destination buffer overflow");
|
data/lib/unicorn.rb
CHANGED
@@ -1,836 +1,83 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
|
3
2
|
require 'fcntl'
|
4
3
|
require 'etc'
|
5
4
|
require 'stringio'
|
6
5
|
require 'rack'
|
7
|
-
require '
|
8
|
-
require 'unicorn/const'
|
9
|
-
require 'unicorn/http_request'
|
10
|
-
require 'unicorn/configurator'
|
11
|
-
require 'unicorn/util'
|
12
|
-
require 'unicorn/tee_input'
|
13
|
-
require 'unicorn/http_response'
|
6
|
+
require 'kgio'
|
14
7
|
|
15
|
-
# Unicorn module containing all of the classes (include C extensions) for
|
16
|
-
# a Unicorn web server. It contains a minimalist HTTP server with just
|
17
|
-
# functionality to service web application requests fast as possible.
|
8
|
+
# Unicorn module containing all of the classes (include C extensions) for
|
9
|
+
# running a Unicorn web server. It contains a minimalist HTTP server with just
|
10
|
+
# enough functionality to service web application requests fast as possible.
|
18
11
|
module Unicorn
|
19
|
-
|
20
|
-
|
21
|
-
# application dispatch. This is always raised with an empty backtrace
|
22
|
-
# since there is nothing in the application stack that is responsible
|
23
|
-
# for client shutdowns/disconnects.
|
24
|
-
class ClientShutdown < EOFError
|
25
|
-
end
|
26
|
-
|
27
|
-
class << self
|
28
|
-
def run(app, options = {})
|
29
|
-
HttpServer.new(app, options).start.join
|
30
|
-
end
|
31
|
-
|
32
|
-
# This returns a lambda to pass in as the app, this does not "build" the
|
33
|
-
# app (which we defer based on the outcome of "preload_app" in the
|
34
|
-
# Unicorn config). The returned lambda will be called when it is
|
35
|
-
# time to build the app.
|
36
|
-
def builder(ru, opts)
|
37
|
-
# allow Configurator to parse cli switches embedded in the ru file
|
38
|
-
Unicorn::Configurator::RACKUP.update(:file => ru, :optparse => opts)
|
39
|
-
|
40
|
-
# always called after config file parsing, may be called after forking
|
41
|
-
lambda do ||
|
42
|
-
inner_app = case ru
|
43
|
-
when /\.ru$/
|
44
|
-
raw = File.read(ru)
|
45
|
-
raw.sub!(/^__END__\n.*/, '')
|
46
|
-
eval("Rack::Builder.new {(#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
|
47
|
-
else
|
48
|
-
require ru
|
49
|
-
Object.const_get(File.basename(ru, '.rb').capitalize)
|
50
|
-
end
|
51
|
-
|
52
|
-
pp({ :inner_app => inner_app }) if $DEBUG
|
53
|
-
|
54
|
-
# return value, matches rackup defaults based on env
|
55
|
-
case ENV["RACK_ENV"]
|
56
|
-
when "development"
|
57
|
-
Rack::Builder.new do
|
58
|
-
use Rack::CommonLogger, $stderr
|
59
|
-
use Rack::ShowExceptions
|
60
|
-
use Rack::Lint
|
61
|
-
run inner_app
|
62
|
-
end.to_app
|
63
|
-
when "deployment"
|
64
|
-
Rack::Builder.new do
|
65
|
-
use Rack::CommonLogger, $stderr
|
66
|
-
run inner_app
|
67
|
-
end.to_app
|
68
|
-
else
|
69
|
-
inner_app
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# returns an array of strings representing TCP listen socket addresses
|
75
|
-
# and Unix domain socket paths. This is useful for use with
|
76
|
-
# Raindrops::Middleware under Linux: http://raindrops.bogomips.org/
|
77
|
-
def listener_names
|
78
|
-
HttpServer::LISTENERS.map { |io| SocketHelper.sock_name(io) }
|
79
|
-
end
|
12
|
+
def self.run(app, options = {})
|
13
|
+
Unicorn::HttpServer.new(app, options).start.join
|
80
14
|
end
|
81
15
|
|
82
|
-
# This
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
# all bound listener sockets
|
98
|
-
LISTENERS = []
|
99
|
-
|
100
|
-
# This hash maps PIDs to Workers
|
101
|
-
WORKERS = {}
|
102
|
-
|
103
|
-
# We use SELF_PIPE differently in the master and worker processes:
|
104
|
-
#
|
105
|
-
# * The master process never closes or reinitializes this once
|
106
|
-
# initialized. Signal handlers in the master process will write to
|
107
|
-
# it to wake up the master from IO.select in exactly the same manner
|
108
|
-
# djb describes in http://cr.yp.to/docs/selfpipe.html
|
109
|
-
#
|
110
|
-
# * The workers immediately close the pipe they inherit from the
|
111
|
-
# master and replace it with a new pipe after forking. This new
|
112
|
-
# pipe is also used to wakeup from IO.select from inside (worker)
|
113
|
-
# signal handlers. However, workers *close* the pipe descriptors in
|
114
|
-
# the signal handlers to raise EBADF in IO.select instead of writing
|
115
|
-
# like we do in the master. We cannot easily use the reader set for
|
116
|
-
# IO.select because LISTENERS is already that set, and it's extra
|
117
|
-
# work (and cycles) to distinguish the pipe FD from the reader set
|
118
|
-
# once IO.select returns. So we're lazy and just close the pipe when
|
119
|
-
# a (rare) signal arrives in the worker and reinitialize the pipe later.
|
120
|
-
SELF_PIPE = []
|
121
|
-
|
122
|
-
# signal queue used for self-piping
|
123
|
-
SIG_QUEUE = []
|
124
|
-
|
125
|
-
# constant lookups are faster and we're single-threaded/non-reentrant
|
126
|
-
REQUEST = HttpRequest.new
|
127
|
-
|
128
|
-
# We populate this at startup so we can figure out how to reexecute
|
129
|
-
# and upgrade the currently running instance of Unicorn
|
130
|
-
# This Hash is considered a stable interface and changing its contents
|
131
|
-
# will allow you to switch between different installations of Unicorn
|
132
|
-
# or even different installations of the same applications without
|
133
|
-
# downtime. Keys of this constant Hash are described as follows:
|
134
|
-
#
|
135
|
-
# * 0 - the path to the unicorn/unicorn_rails executable
|
136
|
-
# * :argv - a deep copy of the ARGV array the executable originally saw
|
137
|
-
# * :cwd - the working directory of the application, this is where
|
138
|
-
# you originally started Unicorn.
|
139
|
-
#
|
140
|
-
# To change your unicorn executable to a different path without downtime,
|
141
|
-
# you can set the following in your Unicorn config file, HUP and then
|
142
|
-
# continue with the traditional USR2 + QUIT upgrade steps:
|
143
|
-
#
|
144
|
-
# Unicorn::HttpServer::START_CTX[0] = "/home/bofh/1.9.2/bin/unicorn"
|
145
|
-
START_CTX = {
|
146
|
-
:argv => ARGV.map { |arg| arg.dup },
|
147
|
-
:cwd => lambda {
|
148
|
-
# favor ENV['PWD'] since it is (usually) symlink aware for
|
149
|
-
# Capistrano and like systems
|
150
|
-
begin
|
151
|
-
a = File.stat(pwd = ENV['PWD'])
|
152
|
-
b = File.stat(Dir.pwd)
|
153
|
-
a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
|
154
|
-
rescue
|
155
|
-
Dir.pwd
|
156
|
-
end
|
157
|
-
}.call,
|
158
|
-
0 => $0.dup,
|
159
|
-
}
|
160
|
-
|
161
|
-
# This class and its members can be considered a stable interface
|
162
|
-
# and will not change in a backwards-incompatible fashion between
|
163
|
-
# releases of Unicorn. You may need to access it in the
|
164
|
-
# before_fork/after_fork hooks. See the Unicorn::Configurator RDoc
|
165
|
-
# for examples.
|
166
|
-
class Worker < Struct.new(:nr, :tmp, :switched)
|
167
|
-
|
168
|
-
# worker objects may be compared to just plain numbers
|
169
|
-
def ==(other_nr)
|
170
|
-
self.nr == other_nr
|
171
|
-
end
|
172
|
-
|
173
|
-
# Changes the worker process to the specified +user+ and +group+
|
174
|
-
# This is only intended to be called from within the worker
|
175
|
-
# process from the +after_fork+ hook. This should be called in
|
176
|
-
# the +after_fork+ hook after any priviledged functions need to be
|
177
|
-
# run (e.g. to set per-worker CPU affinity, niceness, etc)
|
178
|
-
#
|
179
|
-
# Any and all errors raised within this method will be propagated
|
180
|
-
# directly back to the caller (usually the +after_fork+ hook.
|
181
|
-
# These errors commonly include ArgumentError for specifying an
|
182
|
-
# invalid user/group and Errno::EPERM for insufficient priviledges
|
183
|
-
def user(user, group = nil)
|
184
|
-
# we do not protect the caller, checking Process.euid == 0 is
|
185
|
-
# insufficient because modern systems have fine-grained
|
186
|
-
# capabilities. Let the caller handle any and all errors.
|
187
|
-
uid = Etc.getpwnam(user).uid
|
188
|
-
gid = Etc.getgrnam(group).gid if group
|
189
|
-
Unicorn::Util.chown_logs(uid, gid)
|
190
|
-
tmp.chown(uid, gid)
|
191
|
-
if gid && Process.egid != gid
|
192
|
-
Process.initgroups(user, gid)
|
193
|
-
Process::GID.change_privilege(gid)
|
194
|
-
end
|
195
|
-
Process.euid != uid and Process::UID.change_privilege(uid)
|
196
|
-
self.switched = true
|
197
|
-
end
|
198
|
-
|
199
|
-
end
|
200
|
-
|
201
|
-
# Creates a working server on host:port (strange things happen if
|
202
|
-
# port isn't a Number). Use HttpServer::run to start the server and
|
203
|
-
# HttpServer.run.join to join the thread that's processing
|
204
|
-
# incoming requests on the socket.
|
205
|
-
def initialize(app, options = {})
|
206
|
-
self.app = app
|
207
|
-
self.reexec_pid = 0
|
208
|
-
self.ready_pipe = options.delete(:ready_pipe)
|
209
|
-
self.init_listeners = options[:listeners] ? options[:listeners].dup : []
|
210
|
-
self.config = Configurator.new(options.merge(:use_defaults => true))
|
211
|
-
self.listener_opts = {}
|
212
|
-
|
213
|
-
# we try inheriting listeners first, so we bind them later.
|
214
|
-
# we don't write the pid file until we've bound listeners in case
|
215
|
-
# unicorn was started twice by mistake. Even though our #pid= method
|
216
|
-
# checks for stale/existing pid files, race conditions are still
|
217
|
-
# possible (and difficult/non-portable to avoid) and can be likely
|
218
|
-
# to clobber the pid if the second start was in quick succession
|
219
|
-
# after the first, so we rely on the listener binding to fail in
|
220
|
-
# that case. Some tests (in and outside of this source tree) and
|
221
|
-
# monitoring tools may also rely on pid files existing before we
|
222
|
-
# attempt to connect to the listener(s)
|
223
|
-
config.commit!(self, :skip => [:listeners, :pid])
|
224
|
-
self.orig_app = app
|
225
|
-
end
|
226
|
-
|
227
|
-
# Runs the thing. Returns self so you can run join on it
|
228
|
-
def start
|
229
|
-
BasicSocket.do_not_reverse_lookup = true
|
230
|
-
|
231
|
-
# inherit sockets from parents, they need to be plain Socket objects
|
232
|
-
# before they become UNIXServer or TCPServer
|
233
|
-
inherited = ENV['UNICORN_FD'].to_s.split(/,/).map do |fd|
|
234
|
-
io = Socket.for_fd(fd.to_i)
|
235
|
-
set_server_sockopt(io, listener_opts[sock_name(io)])
|
236
|
-
IO_PURGATORY << io
|
237
|
-
logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
|
238
|
-
server_cast(io)
|
239
|
-
end
|
240
|
-
|
241
|
-
config_listeners = config[:listeners].dup
|
242
|
-
LISTENERS.replace(inherited)
|
243
|
-
|
244
|
-
# we start out with generic Socket objects that get cast to either
|
245
|
-
# TCPServer or UNIXServer objects; but since the Socket objects
|
246
|
-
# share the same OS-level file descriptor as the higher-level *Server
|
247
|
-
# objects; we need to prevent Socket objects from being garbage-collected
|
248
|
-
config_listeners -= listener_names
|
249
|
-
if config_listeners.empty? && LISTENERS.empty?
|
250
|
-
config_listeners << Unicorn::Const::DEFAULT_LISTEN
|
251
|
-
init_listeners << Unicorn::Const::DEFAULT_LISTEN
|
252
|
-
START_CTX[:argv] << "-l#{Unicorn::Const::DEFAULT_LISTEN}"
|
253
|
-
end
|
254
|
-
config_listeners.each { |addr| listen(addr) }
|
255
|
-
raise ArgumentError, "no listeners" if LISTENERS.empty?
|
256
|
-
|
257
|
-
# this pipe is used to wake us up from select(2) in #join when signals
|
258
|
-
# are trapped. See trap_deferred.
|
259
|
-
init_self_pipe!
|
260
|
-
|
261
|
-
# setup signal handlers before writing pid file in case people get
|
262
|
-
# trigger happy and send signals as soon as the pid file exists.
|
263
|
-
# Note that signals don't actually get handled until the #join method
|
264
|
-
QUEUE_SIGS.each { |sig| trap_deferred(sig) }
|
265
|
-
trap(:CHLD) { |_| awaken_master }
|
266
|
-
self.pid = config[:pid]
|
267
|
-
|
268
|
-
self.master_pid = $$
|
269
|
-
build_app! if preload_app
|
270
|
-
maintain_worker_count
|
271
|
-
self
|
272
|
-
end
|
273
|
-
|
274
|
-
# replaces current listener set with +listeners+. This will
|
275
|
-
# close the socket if it will not exist in the new listener set
|
276
|
-
def listeners=(listeners)
|
277
|
-
cur_names, dead_names = [], []
|
278
|
-
listener_names.each do |name|
|
279
|
-
if ?/ == name[0]
|
280
|
-
# mark unlinked sockets as dead so we can rebind them
|
281
|
-
(File.socket?(name) ? cur_names : dead_names) << name
|
282
|
-
else
|
283
|
-
cur_names << name
|
284
|
-
end
|
285
|
-
end
|
286
|
-
set_names = listener_names(listeners)
|
287
|
-
dead_names.concat(cur_names - set_names).uniq!
|
288
|
-
|
289
|
-
LISTENERS.delete_if do |io|
|
290
|
-
if dead_names.include?(sock_name(io))
|
291
|
-
IO_PURGATORY.delete_if do |pio|
|
292
|
-
pio.fileno == io.fileno && (pio.close rescue nil).nil? # true
|
293
|
-
end
|
294
|
-
(io.close rescue nil).nil? # true
|
295
|
-
else
|
296
|
-
set_server_sockopt(io, listener_opts[sock_name(io)])
|
297
|
-
false
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
(set_names - cur_names).each { |addr| listen(addr) }
|
302
|
-
end
|
303
|
-
|
304
|
-
def stdout_path=(path); redirect_io($stdout, path); end
|
305
|
-
def stderr_path=(path); redirect_io($stderr, path); end
|
306
|
-
|
307
|
-
def logger=(obj)
|
308
|
-
HttpRequest::DEFAULTS["rack.logger"] = super
|
309
|
-
end
|
310
|
-
|
311
|
-
# sets the path for the PID file of the master process
|
312
|
-
def pid=(path)
|
313
|
-
if path
|
314
|
-
if x = valid_pid?(path)
|
315
|
-
return path if pid && path == pid && x == $$
|
316
|
-
if x == reexec_pid && pid =~ /\.oldbin\z/
|
317
|
-
logger.warn("will not set pid=#{path} while reexec-ed "\
|
318
|
-
"child is running PID:#{x}")
|
319
|
-
return
|
320
|
-
end
|
321
|
-
raise ArgumentError, "Already running on PID:#{x} " \
|
322
|
-
"(or pid=#{path} is stale)"
|
323
|
-
end
|
324
|
-
end
|
325
|
-
unlink_pid_safe(pid) if pid
|
326
|
-
|
327
|
-
if path
|
328
|
-
fp = begin
|
329
|
-
tmp = "#{File.dirname(path)}/#{rand}.#$$"
|
330
|
-
File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
|
331
|
-
rescue Errno::EEXIST
|
332
|
-
retry
|
333
|
-
end
|
334
|
-
fp.syswrite("#$$\n")
|
335
|
-
File.rename(fp.path, path)
|
336
|
-
fp.close
|
337
|
-
end
|
338
|
-
super(path)
|
339
|
-
end
|
340
|
-
|
341
|
-
# add a given address to the +listeners+ set, idempotently
|
342
|
-
# Allows workers to add a private, per-process listener via the
|
343
|
-
# after_fork hook. Very useful for debugging and testing.
|
344
|
-
# +:tries+ may be specified as an option for the number of times
|
345
|
-
# to retry, and +:delay+ may be specified as the time in seconds
|
346
|
-
# to delay between retries.
|
347
|
-
# A negative value for +:tries+ indicates the listen will be
|
348
|
-
# retried indefinitely, this is useful when workers belonging to
|
349
|
-
# different masters are spawned during a transparent upgrade.
|
350
|
-
def listen(address, opt = {}.merge(listener_opts[address] || {}))
|
351
|
-
address = config.expand_addr(address)
|
352
|
-
return if String === address && listener_names.include?(address)
|
353
|
-
|
354
|
-
delay = opt[:delay] || 0.5
|
355
|
-
tries = opt[:tries] || 5
|
356
|
-
begin
|
357
|
-
io = bind_listen(address, opt)
|
358
|
-
unless TCPServer === io || UNIXServer === io
|
359
|
-
IO_PURGATORY << io
|
360
|
-
io = server_cast(io)
|
361
|
-
end
|
362
|
-
logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
|
363
|
-
LISTENERS << io
|
364
|
-
io
|
365
|
-
rescue Errno::EADDRINUSE => err
|
366
|
-
logger.error "adding listener failed addr=#{address} (in use)"
|
367
|
-
raise err if tries == 0
|
368
|
-
tries -= 1
|
369
|
-
logger.error "retrying in #{delay} seconds " \
|
370
|
-
"(#{tries < 0 ? 'infinite' : tries} tries left)"
|
371
|
-
sleep(delay)
|
372
|
-
retry
|
373
|
-
rescue => err
|
374
|
-
logger.fatal "error adding listener addr=#{address}"
|
375
|
-
raise err
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
# monitors children and receives signals forever
|
380
|
-
# (or until a termination signal is sent). This handles signals
|
381
|
-
# one-at-a-time time and we'll happily drop signals in case somebody
|
382
|
-
# is signalling us too often.
|
383
|
-
def join
|
384
|
-
respawn = true
|
385
|
-
last_check = Time.now
|
386
|
-
|
387
|
-
proc_name 'master'
|
388
|
-
logger.info "master process ready" # test_exec.rb relies on this message
|
389
|
-
if ready_pipe
|
390
|
-
ready_pipe.syswrite($$.to_s)
|
391
|
-
ready_pipe.close rescue nil
|
392
|
-
self.ready_pipe = nil
|
393
|
-
end
|
394
|
-
begin
|
395
|
-
loop do
|
396
|
-
reap_all_workers
|
397
|
-
case SIG_QUEUE.shift
|
398
|
-
when nil
|
399
|
-
# avoid murdering workers after our master process (or the
|
400
|
-
# machine) comes out of suspend/hibernation
|
401
|
-
if (last_check + timeout) >= (last_check = Time.now)
|
402
|
-
murder_lazy_workers
|
403
|
-
else
|
404
|
-
# wait for workers to wakeup on suspend
|
405
|
-
master_sleep(timeout/2.0 + 1)
|
406
|
-
end
|
407
|
-
maintain_worker_count if respawn
|
408
|
-
master_sleep(1)
|
409
|
-
when :QUIT # graceful shutdown
|
410
|
-
break
|
411
|
-
when :TERM, :INT # immediate shutdown
|
412
|
-
stop(false)
|
413
|
-
break
|
414
|
-
when :USR1 # rotate logs
|
415
|
-
logger.info "master reopening logs..."
|
416
|
-
Unicorn::Util.reopen_logs
|
417
|
-
logger.info "master done reopening logs"
|
418
|
-
kill_each_worker(:USR1)
|
419
|
-
when :USR2 # exec binary, stay alive in case something went wrong
|
420
|
-
reexec
|
421
|
-
when :WINCH
|
422
|
-
if Process.ppid == 1 || Process.getpgrp != $$
|
423
|
-
respawn = false
|
424
|
-
logger.info "gracefully stopping all workers"
|
425
|
-
kill_each_worker(:QUIT)
|
426
|
-
self.worker_processes = 0
|
427
|
-
else
|
428
|
-
logger.info "SIGWINCH ignored because we're not daemonized"
|
429
|
-
end
|
430
|
-
when :TTIN
|
431
|
-
respawn = true
|
432
|
-
self.worker_processes += 1
|
433
|
-
when :TTOU
|
434
|
-
self.worker_processes -= 1 if self.worker_processes > 0
|
435
|
-
when :HUP
|
436
|
-
respawn = true
|
437
|
-
if config.config_file
|
438
|
-
load_config!
|
439
|
-
redo # immediate reaping since we may have QUIT workers
|
440
|
-
else # exec binary and exit if there's no config file
|
441
|
-
logger.info "config_file not present, reexecuting binary"
|
442
|
-
reexec
|
443
|
-
break
|
444
|
-
end
|
445
|
-
end
|
446
|
-
end
|
447
|
-
rescue Errno::EINTR
|
448
|
-
retry
|
449
|
-
rescue => e
|
450
|
-
logger.error "Unhandled master loop exception #{e.inspect}."
|
451
|
-
logger.error e.backtrace.join("\n")
|
452
|
-
retry
|
453
|
-
end
|
454
|
-
stop # gracefully shutdown all workers on our way out
|
455
|
-
logger.info "master complete"
|
456
|
-
unlink_pid_safe(pid) if pid
|
457
|
-
end
|
458
|
-
|
459
|
-
# Terminates all workers, but does not exit master process
|
460
|
-
def stop(graceful = true)
|
461
|
-
self.listeners = []
|
462
|
-
limit = Time.now + timeout
|
463
|
-
until WORKERS.empty? || Time.now > limit
|
464
|
-
kill_each_worker(graceful ? :QUIT : :TERM)
|
465
|
-
sleep(0.1)
|
466
|
-
reap_all_workers
|
467
|
-
end
|
468
|
-
kill_each_worker(:KILL)
|
469
|
-
end
|
470
|
-
|
471
|
-
private
|
472
|
-
|
473
|
-
# list of signals we care about and trap in master.
|
474
|
-
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP,
|
475
|
-
:TTIN, :TTOU ]
|
476
|
-
|
477
|
-
# defer a signal for later processing in #join (master process)
|
478
|
-
def trap_deferred(signal)
|
479
|
-
trap(signal) do |sig_nr|
|
480
|
-
if SIG_QUEUE.size < 5
|
481
|
-
SIG_QUEUE << signal
|
482
|
-
awaken_master
|
483
|
-
else
|
484
|
-
logger.error "ignoring SIG#{signal}, queue=#{SIG_QUEUE.inspect}"
|
485
|
-
end
|
486
|
-
end
|
487
|
-
end
|
488
|
-
|
489
|
-
# wait for a signal hander to wake us up and then consume the pipe
|
490
|
-
# Wake up every second anyways to run murder_lazy_workers
|
491
|
-
def master_sleep(sec)
|
492
|
-
IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
|
493
|
-
SELF_PIPE[0].read_nonblock(Const::CHUNK_SIZE, HttpRequest::BUF)
|
494
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
495
|
-
end
|
496
|
-
|
497
|
-
def awaken_master
|
498
|
-
begin
|
499
|
-
SELF_PIPE[1].write_nonblock('.') # wakeup master process from select
|
500
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
501
|
-
# pipe is full, master should wake up anyways
|
502
|
-
retry
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
# reaps all unreaped workers
|
507
|
-
def reap_all_workers
|
508
|
-
begin
|
509
|
-
loop do
|
510
|
-
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
511
|
-
wpid or break
|
512
|
-
if reexec_pid == wpid
|
513
|
-
logger.error "reaped #{status.inspect} exec()-ed"
|
514
|
-
self.reexec_pid = 0
|
515
|
-
self.pid = pid.chomp('.oldbin') if pid
|
516
|
-
proc_name 'master'
|
517
|
-
else
|
518
|
-
worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
|
519
|
-
logger.info "reaped #{status.inspect} " \
|
520
|
-
"worker=#{worker.nr rescue 'unknown'}"
|
521
|
-
end
|
522
|
-
end
|
523
|
-
rescue Errno::ECHILD
|
524
|
-
end
|
525
|
-
end
|
526
|
-
|
527
|
-
# reexecutes the START_CTX with a new binary
|
528
|
-
def reexec
|
529
|
-
if reexec_pid > 0
|
530
|
-
begin
|
531
|
-
Process.kill(0, reexec_pid)
|
532
|
-
logger.error "reexec-ed child already running PID:#{reexec_pid}"
|
533
|
-
return
|
534
|
-
rescue Errno::ESRCH
|
535
|
-
self.reexec_pid = 0
|
536
|
-
end
|
537
|
-
end
|
538
|
-
|
539
|
-
if pid
|
540
|
-
old_pid = "#{pid}.oldbin"
|
541
|
-
prev_pid = pid.dup
|
542
|
-
begin
|
543
|
-
self.pid = old_pid # clear the path for a new pid file
|
544
|
-
rescue ArgumentError
|
545
|
-
logger.error "old PID:#{valid_pid?(old_pid)} running with " \
|
546
|
-
"existing pid=#{old_pid}, refusing rexec"
|
547
|
-
return
|
548
|
-
rescue => e
|
549
|
-
logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
|
550
|
-
return
|
551
|
-
end
|
552
|
-
end
|
553
|
-
|
554
|
-
self.reexec_pid = fork do
|
555
|
-
listener_fds = LISTENERS.map { |sock| sock.fileno }
|
556
|
-
ENV['UNICORN_FD'] = listener_fds.join(',')
|
557
|
-
Dir.chdir(START_CTX[:cwd])
|
558
|
-
cmd = [ START_CTX[0] ].concat(START_CTX[:argv])
|
559
|
-
|
560
|
-
# avoid leaking FDs we don't know about, but let before_exec
|
561
|
-
# unset FD_CLOEXEC, if anything else in the app eventually
|
562
|
-
# relies on FD inheritence.
|
563
|
-
(3..1024).each do |io|
|
564
|
-
next if listener_fds.include?(io)
|
565
|
-
io = IO.for_fd(io) rescue nil
|
566
|
-
io or next
|
567
|
-
IO_PURGATORY << io
|
568
|
-
io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
569
|
-
end
|
570
|
-
logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
|
571
|
-
before_exec.call(self)
|
572
|
-
exec(*cmd)
|
573
|
-
end
|
574
|
-
proc_name 'master (old)'
|
575
|
-
end
|
576
|
-
|
577
|
-
# forcibly terminate all workers that haven't checked in in timeout
|
578
|
-
# seconds. The timeout is implemented using an unlinked File
|
579
|
-
# shared between the parent process and each worker. The worker
|
580
|
-
# runs File#chmod to modify the ctime of the File. If the ctime
|
581
|
-
# is stale for >timeout seconds, then we'll kill the corresponding
|
582
|
-
# worker.
|
583
|
-
def murder_lazy_workers
|
584
|
-
WORKERS.dup.each_pair do |wpid, worker|
|
585
|
-
stat = worker.tmp.stat
|
586
|
-
# skip workers that disable fchmod or have never fchmod-ed
|
587
|
-
stat.mode == 0100600 and next
|
588
|
-
(diff = (Time.now - stat.ctime)) <= timeout and next
|
589
|
-
logger.error "worker=#{worker.nr} PID:#{wpid} timeout " \
|
590
|
-
"(#{diff}s > #{timeout}s), killing"
|
591
|
-
kill_worker(:KILL, wpid) # take no prisoners for timeout violations
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
def spawn_missing_workers
|
596
|
-
(0...worker_processes).each do |worker_nr|
|
597
|
-
WORKERS.values.include?(worker_nr) and next
|
598
|
-
worker = Worker.new(worker_nr, Unicorn::Util.tmpio)
|
599
|
-
before_fork.call(self, worker)
|
600
|
-
WORKERS[fork {
|
601
|
-
ready_pipe.close if ready_pipe
|
602
|
-
self.ready_pipe = nil
|
603
|
-
worker_loop(worker)
|
604
|
-
}] = worker
|
605
|
-
end
|
606
|
-
end
|
607
|
-
|
608
|
-
def maintain_worker_count
|
609
|
-
(off = WORKERS.size - worker_processes) == 0 and return
|
610
|
-
off < 0 and return spawn_missing_workers
|
611
|
-
WORKERS.dup.each_pair { |wpid,w|
|
612
|
-
w.nr >= worker_processes and kill_worker(:QUIT, wpid) rescue nil
|
613
|
-
}
|
614
|
-
end
|
615
|
-
|
616
|
-
# if we get any error, try to write something back to the client
|
617
|
-
# assuming we haven't closed the socket, but don't get hung up
|
618
|
-
# if the socket is already closed or broken. We'll always ensure
|
619
|
-
# the socket is closed at the end of this function
|
620
|
-
def handle_error(client, e)
|
621
|
-
msg = case e
|
622
|
-
when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
|
623
|
-
Const::ERROR_500_RESPONSE
|
624
|
-
when HttpParserError # try to tell the client they're bad
|
625
|
-
Const::ERROR_400_RESPONSE
|
16
|
+
# This returns a lambda to pass in as the app, this does not "build" the
|
17
|
+
# app (which we defer based on the outcome of "preload_app" in the
|
18
|
+
# Unicorn config). The returned lambda will be called when it is
|
19
|
+
# time to build the app.
|
20
|
+
def self.builder(ru, opts)
|
21
|
+
# allow Configurator to parse cli switches embedded in the ru file
|
22
|
+
Unicorn::Configurator::RACKUP.update(:file => ru, :optparse => opts)
|
23
|
+
|
24
|
+
# always called after config file parsing, may be called after forking
|
25
|
+
lambda do ||
|
26
|
+
inner_app = case ru
|
27
|
+
when /\.ru$/
|
28
|
+
raw = File.read(ru)
|
29
|
+
raw.sub!(/^__END__\n.*/, '')
|
30
|
+
eval("Rack::Builder.new {(#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
|
626
31
|
else
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
client.close # flushes and uncorks the socket immediately, no keepalive
|
650
|
-
rescue => e
|
651
|
-
handle_error(client, e)
|
652
|
-
end
|
653
|
-
|
654
|
-
# gets rid of stuff the worker has no business keeping track of
|
655
|
-
# to free some resources and drops all sig handlers.
|
656
|
-
# traps for USR1, USR2, and HUP may be set in the after_fork Proc
|
657
|
-
# by the user.
|
658
|
-
def init_worker_process(worker)
|
659
|
-
QUEUE_SIGS.each { |sig| trap(sig, nil) }
|
660
|
-
trap(:CHLD, 'DEFAULT')
|
661
|
-
SIG_QUEUE.clear
|
662
|
-
proc_name "worker[#{worker.nr}]"
|
663
|
-
START_CTX.clear
|
664
|
-
init_self_pipe!
|
665
|
-
WORKERS.values.each { |other| other.tmp.close rescue nil }
|
666
|
-
WORKERS.clear
|
667
|
-
LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
668
|
-
worker.tmp.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
669
|
-
after_fork.call(self, worker) # can drop perms
|
670
|
-
worker.user(*user) if user.kind_of?(Array) && ! worker.switched
|
671
|
-
self.timeout /= 2.0 # halve it for select()
|
672
|
-
build_app! unless preload_app
|
673
|
-
end
|
674
|
-
|
675
|
-
def reopen_worker_logs(worker_nr)
|
676
|
-
logger.info "worker=#{worker_nr} reopening logs..."
|
677
|
-
Unicorn::Util.reopen_logs
|
678
|
-
logger.info "worker=#{worker_nr} done reopening logs"
|
679
|
-
init_self_pipe!
|
680
|
-
end
|
681
|
-
|
682
|
-
# runs inside each forked worker, this sits around and waits
|
683
|
-
# for connections and doesn't die until the parent dies (or is
|
684
|
-
# given a INT, QUIT, or TERM signal)
|
685
|
-
def worker_loop(worker)
|
686
|
-
ppid = master_pid
|
687
|
-
init_worker_process(worker)
|
688
|
-
nr = 0 # this becomes negative if we need to reopen logs
|
689
|
-
alive = worker.tmp # tmp is our lifeline to the master process
|
690
|
-
ready = LISTENERS
|
691
|
-
|
692
|
-
# closing anything we IO.select on will raise EBADF
|
693
|
-
trap(:USR1) { nr = -65536; SELF_PIPE[0].close rescue nil }
|
694
|
-
trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil } }
|
695
|
-
[:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
|
696
|
-
logger.info "worker=#{worker.nr} ready"
|
697
|
-
m = 0
|
698
|
-
|
699
|
-
begin
|
700
|
-
nr < 0 and reopen_worker_logs(worker.nr)
|
701
|
-
nr = 0
|
702
|
-
|
703
|
-
# we're a goner in timeout seconds anyways if alive.chmod
|
704
|
-
# breaks, so don't trap the exception. Using fchmod() since
|
705
|
-
# futimes() is not available in base Ruby and I very strongly
|
706
|
-
# prefer temporary files to be unlinked for security,
|
707
|
-
# performance and reliability reasons, so utime is out. No-op
|
708
|
-
# changes with chmod doesn't update ctime on all filesystems; so
|
709
|
-
# we change our counter each and every time (after process_client
|
710
|
-
# and before IO.select).
|
711
|
-
alive.chmod(m = 0 == m ? 1 : 0)
|
712
|
-
|
713
|
-
ready.each do |sock|
|
714
|
-
begin
|
715
|
-
process_client(sock.accept_nonblock)
|
716
|
-
nr += 1
|
717
|
-
alive.chmod(m = 0 == m ? 1 : 0)
|
718
|
-
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
719
|
-
end
|
720
|
-
break if nr < 0
|
721
|
-
end
|
722
|
-
|
723
|
-
# make the following bet: if we accepted clients this round,
|
724
|
-
# we're probably reasonably busy, so avoid calling select()
|
725
|
-
# and do a speculative accept_nonblock on ready listeners
|
726
|
-
# before we sleep again in select().
|
727
|
-
redo unless nr == 0 # (nr < 0) => reopen logs
|
728
|
-
|
729
|
-
ppid == Process.ppid or return
|
730
|
-
alive.chmod(m = 0 == m ? 1 : 0)
|
731
|
-
begin
|
732
|
-
# timeout used so we can detect parent death:
|
733
|
-
ret = IO.select(LISTENERS, nil, SELF_PIPE, timeout) or redo
|
734
|
-
ready = ret[0]
|
735
|
-
rescue Errno::EINTR
|
736
|
-
ready = LISTENERS
|
737
|
-
rescue Errno::EBADF
|
738
|
-
nr < 0 or return
|
739
|
-
end
|
740
|
-
rescue => e
|
741
|
-
if alive
|
742
|
-
logger.error "Unhandled listen loop exception #{e.inspect}."
|
743
|
-
logger.error e.backtrace.join("\n")
|
744
|
-
end
|
745
|
-
end while alive
|
746
|
-
end
|
747
|
-
|
748
|
-
# delivers a signal to a worker and fails gracefully if the worker
|
749
|
-
# is no longer running.
|
750
|
-
def kill_worker(signal, wpid)
|
751
|
-
begin
|
752
|
-
Process.kill(signal, wpid)
|
753
|
-
rescue Errno::ESRCH
|
754
|
-
worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil
|
755
|
-
end
|
756
|
-
end
|
757
|
-
|
758
|
-
# delivers a signal to each worker
|
759
|
-
def kill_each_worker(signal)
|
760
|
-
WORKERS.keys.each { |wpid| kill_worker(signal, wpid) }
|
761
|
-
end
|
762
|
-
|
763
|
-
# unlinks a PID file at given +path+ if it contains the current PID
|
764
|
-
# still potentially racy without locking the directory (which is
|
765
|
-
# non-portable and may interact badly with other programs), but the
|
766
|
-
# window for hitting the race condition is small
|
767
|
-
def unlink_pid_safe(path)
|
768
|
-
(File.read(path).to_i == $$ and File.unlink(path)) rescue nil
|
769
|
-
end
|
770
|
-
|
771
|
-
# returns a PID if a given path contains a non-stale PID file,
|
772
|
-
# nil otherwise.
|
773
|
-
def valid_pid?(path)
|
774
|
-
wpid = File.read(path).to_i
|
775
|
-
wpid <= 0 and return nil
|
776
|
-
begin
|
777
|
-
Process.kill(0, wpid)
|
778
|
-
wpid
|
779
|
-
rescue Errno::ESRCH
|
780
|
-
# don't unlink stale pid files, racy without non-portable locking...
|
781
|
-
end
|
782
|
-
rescue Errno::ENOENT
|
783
|
-
end
|
784
|
-
|
785
|
-
def load_config!
|
786
|
-
loaded_app = app
|
787
|
-
begin
|
788
|
-
logger.info "reloading config_file=#{config.config_file}"
|
789
|
-
config[:listeners].replace(init_listeners)
|
790
|
-
config.reload
|
791
|
-
config.commit!(self)
|
792
|
-
kill_each_worker(:QUIT)
|
793
|
-
Unicorn::Util.reopen_logs
|
794
|
-
self.app = orig_app
|
795
|
-
build_app! if preload_app
|
796
|
-
logger.info "done reloading config_file=#{config.config_file}"
|
797
|
-
rescue StandardError, LoadError, SyntaxError => e
|
798
|
-
logger.error "error reloading config_file=#{config.config_file}: " \
|
799
|
-
"#{e.class} #{e.message} #{e.backtrace}"
|
800
|
-
self.app = loaded_app
|
801
|
-
end
|
802
|
-
end
|
803
|
-
|
804
|
-
# returns an array of string names for the given listener array
|
805
|
-
def listener_names(listeners = LISTENERS)
|
806
|
-
listeners.map { |io| sock_name(io) }
|
807
|
-
end
|
808
|
-
|
809
|
-
def build_app!
|
810
|
-
if app.respond_to?(:arity) && app.arity == 0
|
811
|
-
if defined?(Gem) && Gem.respond_to?(:refresh)
|
812
|
-
logger.info "Refreshing Gem list"
|
813
|
-
Gem.refresh
|
814
|
-
end
|
815
|
-
self.app = app.call
|
32
|
+
require ru
|
33
|
+
Object.const_get(File.basename(ru, '.rb').capitalize)
|
34
|
+
end
|
35
|
+
|
36
|
+
pp({ :inner_app => inner_app }) if $DEBUG
|
37
|
+
|
38
|
+
# return value, matches rackup defaults based on env
|
39
|
+
case ENV["RACK_ENV"]
|
40
|
+
when "development"
|
41
|
+
Rack::Builder.new do
|
42
|
+
use Rack::CommonLogger, $stderr
|
43
|
+
use Rack::ShowExceptions
|
44
|
+
use Rack::Lint
|
45
|
+
run inner_app
|
46
|
+
end.to_app
|
47
|
+
when "deployment"
|
48
|
+
Rack::Builder.new do
|
49
|
+
use Rack::CommonLogger, $stderr
|
50
|
+
run inner_app
|
51
|
+
end.to_app
|
52
|
+
else
|
53
|
+
inner_app
|
816
54
|
end
|
817
55
|
end
|
56
|
+
end
|
818
57
|
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
File.open(path, 'ab') { |fp| io.reopen(fp) } if path
|
826
|
-
io.sync = true
|
827
|
-
end
|
828
|
-
|
829
|
-
def init_self_pipe!
|
830
|
-
SELF_PIPE.each { |io| io.close rescue nil }
|
831
|
-
SELF_PIPE.replace(IO.pipe)
|
832
|
-
SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
|
58
|
+
# returns an array of strings representing TCP listen socket addresses
|
59
|
+
# and Unix domain socket paths. This is useful for use with
|
60
|
+
# Raindrops::Middleware under Linux: http://raindrops.bogomips.org/
|
61
|
+
def self.listener_names
|
62
|
+
Unicorn::HttpServer::LISTENERS.map do |io|
|
63
|
+
Unicorn::SocketHelper.sock_name(io)
|
833
64
|
end
|
834
|
-
|
835
65
|
end
|
836
66
|
end
|
67
|
+
|
68
|
+
# raised inside TeeInput when a client closes the socket inside the
|
69
|
+
# application dispatch. This is always raised with an empty backtrace
|
70
|
+
# since there is nothing in the application stack that is responsible
|
71
|
+
# for client shutdowns/disconnects.
|
72
|
+
class Unicorn::ClientShutdown < EOFError; end
|
73
|
+
|
74
|
+
require 'unicorn/const'
|
75
|
+
require 'unicorn/socket_helper'
|
76
|
+
require 'unicorn/http_request'
|
77
|
+
require 'unicorn/configurator'
|
78
|
+
require 'unicorn/tmpio'
|
79
|
+
require 'unicorn/util'
|
80
|
+
require 'unicorn/tee_input'
|
81
|
+
require 'unicorn/http_response'
|
82
|
+
require 'unicorn/worker'
|
83
|
+
require 'unicorn/http_server'
|