unicorn 5.0.1 → 6.1.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.
- checksums.yaml +5 -5
- data/.manifest +11 -5
- data/.olddoc.yml +16 -6
- data/Application_Timeouts +4 -4
- data/CONTRIBUTORS +6 -2
- data/Documentation/.gitignore +1 -3
- data/Documentation/unicorn.1 +222 -0
- data/Documentation/unicorn_rails.1 +207 -0
- data/FAQ +1 -1
- data/GIT-VERSION-FILE +1 -1
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +118 -58
- data/HACKING +2 -10
- data/ISSUES +40 -35
- data/KNOWN_ISSUES +2 -2
- data/LATEST +23 -28
- data/LICENSE +2 -2
- data/Links +13 -11
- data/NEWS +612 -0
- data/README +30 -29
- data/SIGNALS +1 -1
- data/Sandbox +8 -7
- data/TODO +0 -2
- data/TUNING +19 -1
- data/archive/slrnpull.conf +1 -1
- data/bin/unicorn +3 -1
- data/bin/unicorn_rails +2 -2
- data/examples/big_app_gc.rb +1 -1
- data/examples/init.sh +36 -8
- data/examples/logrotate.conf +17 -2
- data/examples/nginx.conf +4 -3
- data/examples/unicorn.conf.minimal.rb +2 -2
- data/examples/unicorn.conf.rb +2 -2
- data/examples/unicorn@.service +14 -0
- data/ext/unicorn_http/c_util.h +5 -13
- data/ext/unicorn_http/common_field_optimization.h +22 -5
- data/ext/unicorn_http/epollexclusive.h +124 -0
- data/ext/unicorn_http/ext_help.h +0 -44
- data/ext/unicorn_http/extconf.rb +32 -6
- data/ext/unicorn_http/global_variables.h +2 -2
- data/ext/unicorn_http/httpdate.c +2 -1
- data/ext/unicorn_http/unicorn_http.c +853 -498
- data/ext/unicorn_http/unicorn_http.rl +86 -30
- data/ext/unicorn_http/unicorn_http_common.rl +1 -1
- data/lib/unicorn/configurator.rb +93 -13
- data/lib/unicorn/http_request.rb +101 -11
- data/lib/unicorn/http_response.rb +8 -4
- data/lib/unicorn/http_server.rb +141 -72
- data/lib/unicorn/launcher.rb +1 -1
- data/lib/unicorn/oob_gc.rb +6 -6
- data/lib/unicorn/select_waiter.rb +6 -0
- data/lib/unicorn/socket_helper.rb +23 -7
- data/lib/unicorn/stream_input.rb +5 -4
- data/lib/unicorn/tee_input.rb +8 -10
- data/lib/unicorn/tmpio.rb +8 -2
- data/lib/unicorn/util.rb +3 -3
- data/lib/unicorn/version.rb +1 -1
- data/lib/unicorn/worker.rb +33 -8
- data/lib/unicorn.rb +55 -29
- data/man/man1/unicorn.1 +120 -118
- data/man/man1/unicorn_rails.1 +106 -107
- data/t/GNUmakefile +3 -72
- data/t/README +4 -4
- data/t/t0011-active-unix-socket.sh +1 -1
- data/t/t0012-reload-empty-config.sh +2 -1
- data/t/t0301-no-default-middleware-ignored-in-config.sh +25 -0
- data/t/t0301.ru +13 -0
- data/t/test-lib.sh +4 -3
- data/test/benchmark/README +14 -4
- data/test/benchmark/ddstream.ru +50 -0
- data/test/benchmark/readinput.ru +40 -0
- data/test/benchmark/uconnect.perl +66 -0
- data/test/exec/test_exec.rb +26 -24
- data/test/test_helper.rb +38 -30
- data/test/unit/test_ccc.rb +91 -0
- data/test/unit/test_droplet.rb +1 -1
- data/test/unit/test_http_parser.rb +46 -16
- data/test/unit/test_http_parser_ng.rb +81 -0
- data/test/unit/test_request.rb +10 -10
- data/test/unit/test_server.rb +86 -12
- data/test/unit/test_signals.rb +8 -8
- data/test/unit/test_socket_helper.rb +13 -9
- data/test/unit/test_upload.rb +9 -14
- data/test/unit/test_util.rb +31 -5
- data/test/unit/test_waiter.rb +34 -0
- data/unicorn.gemspec +21 -22
- metadata +21 -28
- data/Documentation/GNUmakefile +0 -30
- data/Documentation/unicorn.1.txt +0 -188
- data/Documentation/unicorn_rails.1.txt +0 -175
- data/t/hijack.ru +0 -43
- data/t/t0200-rack-hijack.sh +0 -30
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/perl -w
|
2
|
+
# Benchmark script to spawn some processes and hammer a local unicorn
|
3
|
+
# to test accept loop performance. This only does Unix sockets.
|
4
|
+
# There's plenty of TCP benchmarking tools out there, and TCP port reuse
|
5
|
+
# has predictability problems since unicorn can't do persistent connections.
|
6
|
+
# Written in Perl for the same reason: predictability.
|
7
|
+
# Ruby GC is not as predictable as Perl refcounting.
|
8
|
+
use strict;
|
9
|
+
use Socket qw(AF_UNIX SOCK_STREAM sockaddr_un);
|
10
|
+
use POSIX qw(:sys_wait_h);
|
11
|
+
use Getopt::Std;
|
12
|
+
# -c / -n switches stolen from ab(1)
|
13
|
+
my $usage = "$0 [-c CONCURRENCY] [-n NUM_REQUESTS] SOCKET_PATH\n";
|
14
|
+
our $opt_c = 2;
|
15
|
+
our $opt_n = 1000;
|
16
|
+
getopts('c:n:') or die $usage;
|
17
|
+
my $unix_path = shift or die $usage;
|
18
|
+
use constant REQ => "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
|
19
|
+
use constant REQ_LEN => length(REQ);
|
20
|
+
use constant BUFSIZ => 8192;
|
21
|
+
$^F = 99; # don't waste syscall time with FD_CLOEXEC
|
22
|
+
|
23
|
+
my %workers; # pid => worker num
|
24
|
+
die "-n $opt_n not evenly divisible by -c $opt_c\n" if $opt_n % $opt_c;
|
25
|
+
my $n_per_worker = $opt_n / $opt_c;
|
26
|
+
my $addr = sockaddr_un($unix_path);
|
27
|
+
|
28
|
+
for my $num (1..$opt_c) {
|
29
|
+
defined(my $pid = fork) or die "fork failed: $!\n";
|
30
|
+
if ($pid) {
|
31
|
+
$workers{$pid} = $num;
|
32
|
+
} else {
|
33
|
+
work($n_per_worker);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
reap_worker(0) while scalar keys %workers;
|
38
|
+
exit;
|
39
|
+
|
40
|
+
sub work {
|
41
|
+
my ($n) = @_;
|
42
|
+
my ($buf, $x);
|
43
|
+
for (1..$n) {
|
44
|
+
socket(S, AF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
|
45
|
+
connect(S, $addr) or die "connect: $!";
|
46
|
+
defined($x = syswrite(S, REQ)) or die "write: $!";
|
47
|
+
$x == REQ_LEN or die "short write: $x != ".REQ_LEN."\n";
|
48
|
+
do {
|
49
|
+
$x = sysread(S, $buf, BUFSIZ);
|
50
|
+
unless (defined $x) {
|
51
|
+
next if $!{EINTR};
|
52
|
+
die "sysread: $!\n";
|
53
|
+
}
|
54
|
+
} until ($x == 0);
|
55
|
+
}
|
56
|
+
exit 0;
|
57
|
+
}
|
58
|
+
|
59
|
+
sub reap_worker {
|
60
|
+
my ($flags) = @_;
|
61
|
+
my $pid = waitpid(-1, $flags);
|
62
|
+
return if !defined $pid || $pid <= 0;
|
63
|
+
my $p = delete $workers{$pid} || '(unknown)';
|
64
|
+
warn("$pid [$p] exited with $?\n") if $?;
|
65
|
+
$p;
|
66
|
+
}
|
data/test/exec/test_exec.rb
CHANGED
@@ -45,8 +45,9 @@ end
|
|
45
45
|
|
46
46
|
COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
|
47
47
|
|
48
|
+
HEAVY_WORKERS = 2
|
48
49
|
HEAVY_CFG = <<-EOS
|
49
|
-
worker_processes
|
50
|
+
worker_processes #{HEAVY_WORKERS}
|
50
51
|
timeout 30
|
51
52
|
logger Logger.new('#{COMMON_TMP.path}')
|
52
53
|
before_fork do |server, worker|
|
@@ -97,6 +98,9 @@ run lambda { |env|
|
|
97
98
|
end
|
98
99
|
|
99
100
|
def test_sd_listen_fds_emulation
|
101
|
+
# [ruby-core:69895] [Bug #11336] fixed by r51576
|
102
|
+
return if RUBY_VERSION.to_f < 2.3
|
103
|
+
|
100
104
|
File.open("config.ru", "wb") { |fp| fp.write(HI) }
|
101
105
|
sock = TCPServer.new(@addr, @port)
|
102
106
|
|
@@ -119,14 +123,12 @@ run lambda { |env|
|
|
119
123
|
res = hit(["http://#@addr:#@port/"])
|
120
124
|
assert_equal [ "HI\n" ], res
|
121
125
|
assert_shutdown(pid)
|
122
|
-
|
126
|
+
assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
|
123
127
|
'unicorn should always set SO_KEEPALIVE on inherited sockets'
|
124
128
|
end
|
125
129
|
ensure
|
126
130
|
sock.close if sock
|
127
|
-
|
128
|
-
# [ruby-core:69895] [Bug #11336] fixed by r51576
|
129
|
-
end if RUBY_VERSION.to_f >= 2.3
|
131
|
+
end
|
130
132
|
|
131
133
|
def test_inherit_listener_unspecified
|
132
134
|
File.open("config.ru", "wb") { |fp| fp.write(HI) }
|
@@ -142,7 +144,7 @@ run lambda { |env|
|
|
142
144
|
res = hit(["http://#@addr:#@port/"])
|
143
145
|
assert_equal [ "HI\n" ], res
|
144
146
|
assert_shutdown(pid)
|
145
|
-
|
147
|
+
assert sock.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).bool,
|
146
148
|
'unicorn should always set SO_KEEPALIVE on inherited sockets'
|
147
149
|
ensure
|
148
150
|
sock.close if sock
|
@@ -192,8 +194,8 @@ EOF
|
|
192
194
|
assert_equal other.path, results.first
|
193
195
|
|
194
196
|
Process.kill(:QUIT, pid)
|
195
|
-
|
196
|
-
|
197
|
+
ensure
|
198
|
+
FileUtils.rmtree(other.path)
|
197
199
|
end
|
198
200
|
|
199
201
|
def test_working_directory
|
@@ -228,8 +230,8 @@ EOF
|
|
228
230
|
assert_equal other.path, results.first
|
229
231
|
|
230
232
|
Process.kill(:QUIT, pid)
|
231
|
-
|
232
|
-
|
233
|
+
ensure
|
234
|
+
FileUtils.rmtree(other.path)
|
233
235
|
end
|
234
236
|
|
235
237
|
def test_working_directory_controls_relative_paths
|
@@ -270,11 +272,10 @@ EOF
|
|
270
272
|
wait_master_ready("#{other.path}/stderr_log_here")
|
271
273
|
|
272
274
|
Process.kill(:QUIT, pid)
|
273
|
-
|
274
|
-
|
275
|
+
ensure
|
276
|
+
FileUtils.rmtree(other.path)
|
275
277
|
end
|
276
278
|
|
277
|
-
|
278
279
|
def test_exit_signals
|
279
280
|
%w(INT TERM QUIT).each do |sig|
|
280
281
|
File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
|
@@ -573,7 +574,7 @@ EOF
|
|
573
574
|
assert_equal String, results[0].class
|
574
575
|
worker_pid = results[0].to_i
|
575
576
|
assert_not_equal pid, worker_pid
|
576
|
-
s =
|
577
|
+
s = unix_socket(tmp.path)
|
577
578
|
s.syswrite("GET / HTTP/1.0\r\n\r\n")
|
578
579
|
results = ''
|
579
580
|
loop { results << s.sysread(4096) } rescue nil
|
@@ -606,6 +607,7 @@ EOF
|
|
606
607
|
def test_weird_config_settings
|
607
608
|
File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
|
608
609
|
ucfg = Tempfile.new('unicorn_test_config')
|
610
|
+
proc_total = HEAVY_WORKERS + 1 # + 1 for master
|
609
611
|
ucfg.syswrite(HEAVY_CFG)
|
610
612
|
pid = xfork do
|
611
613
|
redirect_test_io do
|
@@ -616,9 +618,9 @@ EOF
|
|
616
618
|
results = retry_hit(["http://#{@addr}:#{@port}/"])
|
617
619
|
assert_equal String, results[0].class
|
618
620
|
wait_master_ready(COMMON_TMP.path)
|
619
|
-
wait_workers_ready(COMMON_TMP.path,
|
621
|
+
wait_workers_ready(COMMON_TMP.path, HEAVY_WORKERS)
|
620
622
|
bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
|
621
|
-
assert_equal
|
623
|
+
assert_equal HEAVY_WORKERS, bf.size
|
622
624
|
rotate = Tempfile.new('unicorn_rotate')
|
623
625
|
|
624
626
|
File.rename(COMMON_TMP.path, rotate.path)
|
@@ -630,20 +632,20 @@ EOF
|
|
630
632
|
tries = DEFAULT_TRIES
|
631
633
|
log = File.readlines(rotate.path)
|
632
634
|
while (tries -= 1) > 0 &&
|
633
|
-
log.grep(/reopening logs\.\.\./).size <
|
635
|
+
log.grep(/reopening logs\.\.\./).size < proc_total
|
634
636
|
sleep DEFAULT_RES
|
635
637
|
log = File.readlines(rotate.path)
|
636
638
|
end
|
637
|
-
assert_equal
|
639
|
+
assert_equal proc_total, log.grep(/reopening logs\.\.\./).size
|
638
640
|
assert_equal 0, log.grep(/done reopening logs/).size
|
639
641
|
|
640
642
|
tries = DEFAULT_TRIES
|
641
643
|
log = File.readlines(COMMON_TMP.path)
|
642
|
-
while (tries -= 1) > 0 && log.grep(/done reopening logs/).size <
|
644
|
+
while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < proc_total
|
643
645
|
sleep DEFAULT_RES
|
644
646
|
log = File.readlines(COMMON_TMP.path)
|
645
647
|
end
|
646
|
-
assert_equal
|
648
|
+
assert_equal proc_total, log.grep(/done reopening logs/).size
|
647
649
|
assert_equal 0, log.grep(/reopening logs\.\.\./).size
|
648
650
|
|
649
651
|
Process.kill(:QUIT, pid)
|
@@ -730,7 +732,7 @@ EOF
|
|
730
732
|
wait_for_file(sock_path)
|
731
733
|
assert File.socket?(sock_path)
|
732
734
|
|
733
|
-
sock =
|
735
|
+
sock = unix_socket(sock_path)
|
734
736
|
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
|
735
737
|
results = sock.sysread(4096)
|
736
738
|
|
@@ -740,7 +742,7 @@ EOF
|
|
740
742
|
wait_for_file(sock_path)
|
741
743
|
assert File.socket?(sock_path)
|
742
744
|
|
743
|
-
sock =
|
745
|
+
sock = unix_socket(sock_path)
|
744
746
|
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
|
745
747
|
results = sock.sysread(4096)
|
746
748
|
|
@@ -775,7 +777,7 @@ EOF
|
|
775
777
|
assert_equal pid, File.read(pid_file).to_i
|
776
778
|
assert File.socket?(sock_path), "socket created"
|
777
779
|
|
778
|
-
sock =
|
780
|
+
sock = unix_socket(sock_path)
|
779
781
|
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
|
780
782
|
results = sock.sysread(4096)
|
781
783
|
|
@@ -801,7 +803,7 @@ EOF
|
|
801
803
|
wait_for_file(new_sock_path)
|
802
804
|
assert File.socket?(new_sock_path), "socket exists"
|
803
805
|
@sockets.each do |path|
|
804
|
-
sock =
|
806
|
+
sock = unix_socket(path)
|
805
807
|
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
|
806
808
|
results = sock.sysread(4096)
|
807
809
|
assert_equal String, results.class
|
data/test/test_helper.rb
CHANGED
@@ -28,22 +28,43 @@ require 'tempfile'
|
|
28
28
|
require 'fileutils'
|
29
29
|
require 'logger'
|
30
30
|
require 'unicorn'
|
31
|
+
require 'io/nonblock'
|
31
32
|
|
32
33
|
if ENV['DEBUG']
|
33
34
|
require 'ruby-debug'
|
34
35
|
Debugger.start
|
35
36
|
end
|
36
37
|
|
38
|
+
unless RUBY_VERSION < '3.1'
|
39
|
+
warn "Unicorn was only tested against MRI up to 3.0.\n" \
|
40
|
+
"It might not properly work with #{RUBY_VERSION}"
|
41
|
+
end
|
42
|
+
|
37
43
|
def redirect_test_io
|
38
44
|
orig_err = STDERR.dup
|
39
45
|
orig_out = STDOUT.dup
|
40
|
-
|
41
|
-
|
46
|
+
rdr_pid = $$
|
47
|
+
new_out = File.open("test_stdout.#$$.log", "a")
|
48
|
+
new_err = File.open("test_stderr.#$$.log", "a")
|
49
|
+
new_out.sync = new_err.sync = true
|
50
|
+
|
51
|
+
if tail = ENV['TAIL'] # "tail -F" if GNU, "tail -f" otherwise
|
52
|
+
require 'shellwords'
|
53
|
+
cmd = tail.shellsplit
|
54
|
+
cmd << new_out.path
|
55
|
+
cmd << new_err.path
|
56
|
+
pid = Process.spawn(*cmd, { 1 => 2, :pgroup => true })
|
57
|
+
sleep 0.1 # wait for tail(1) to startup
|
58
|
+
end
|
59
|
+
STDERR.reopen(new_err)
|
60
|
+
STDOUT.reopen(new_out)
|
42
61
|
STDERR.sync = STDOUT.sync = true
|
43
62
|
|
44
63
|
at_exit do
|
45
|
-
|
46
|
-
|
64
|
+
if rdr_pid == $$
|
65
|
+
File.unlink(new_out.path) rescue nil
|
66
|
+
File.unlink(new_err.path) rescue nil
|
67
|
+
end
|
47
68
|
end
|
48
69
|
|
49
70
|
begin
|
@@ -51,6 +72,7 @@ def redirect_test_io
|
|
51
72
|
ensure
|
52
73
|
STDERR.reopen(orig_err)
|
53
74
|
STDOUT.reopen(orig_out)
|
75
|
+
Process.kill(:TERM, pid) if pid
|
54
76
|
end
|
55
77
|
end
|
56
78
|
|
@@ -265,34 +287,20 @@ def wait_for_death(pid)
|
|
265
287
|
raise "PID:#{pid} never died!"
|
266
288
|
end
|
267
289
|
|
268
|
-
# executes +cmd+ and chunks its STDOUT
|
269
|
-
def chunked_spawn(stdout, *cmd)
|
270
|
-
fork {
|
271
|
-
crd, cwr = IO.pipe
|
272
|
-
crd.binmode
|
273
|
-
cwr.binmode
|
274
|
-
crd.sync = cwr.sync = true
|
275
|
-
|
276
|
-
pid = fork {
|
277
|
-
STDOUT.reopen(cwr)
|
278
|
-
crd.close
|
279
|
-
cwr.close
|
280
|
-
exec(*cmd)
|
281
|
-
}
|
282
|
-
cwr.close
|
283
|
-
begin
|
284
|
-
buf = crd.readpartial(16384)
|
285
|
-
stdout.write("#{'%x' % buf.size}\r\n#{buf}")
|
286
|
-
rescue EOFError
|
287
|
-
stdout.write("0\r\n")
|
288
|
-
pid, status = Process.waitpid(pid)
|
289
|
-
exit status.exitstatus
|
290
|
-
end while true
|
291
|
-
}
|
292
|
-
end
|
293
|
-
|
294
290
|
def reset_sig_handlers
|
295
291
|
%w(WINCH QUIT INT TERM USR1 USR2 HUP TTIN TTOU CHLD).each do |sig|
|
296
292
|
trap(sig, "DEFAULT")
|
297
293
|
end
|
298
294
|
end
|
295
|
+
|
296
|
+
def tcp_socket(*args)
|
297
|
+
sock = TCPSocket.new(*args)
|
298
|
+
sock.nonblock = false
|
299
|
+
sock
|
300
|
+
end
|
301
|
+
|
302
|
+
def unix_socket(*args)
|
303
|
+
sock = UNIXSocket.new(*args)
|
304
|
+
sock.nonblock = false
|
305
|
+
sock
|
306
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'unicorn'
|
3
|
+
require 'io/wait'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'test/unit'
|
6
|
+
require './test/test_helper'
|
7
|
+
|
8
|
+
class TestCccTCPI < Test::Unit::TestCase
|
9
|
+
def test_ccc_tcpi
|
10
|
+
start_pid = $$
|
11
|
+
host = '127.0.0.1'
|
12
|
+
srv = TCPServer.new(host, 0)
|
13
|
+
port = srv.addr[1]
|
14
|
+
err = Tempfile.new('unicorn_ccc')
|
15
|
+
rd, wr = IO.pipe
|
16
|
+
sleep_pipe = IO.pipe
|
17
|
+
pid = fork do
|
18
|
+
sleep_pipe[1].close
|
19
|
+
reqs = 0
|
20
|
+
rd.close
|
21
|
+
worker_pid = nil
|
22
|
+
app = lambda do |env|
|
23
|
+
worker_pid ||= begin
|
24
|
+
at_exit { wr.write(reqs.to_s) if worker_pid == $$ }
|
25
|
+
$$
|
26
|
+
end
|
27
|
+
reqs += 1
|
28
|
+
|
29
|
+
# will wake up when writer closes
|
30
|
+
sleep_pipe[0].read if env['PATH_INFO'] == '/sleep'
|
31
|
+
|
32
|
+
[ 200, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ]
|
33
|
+
end
|
34
|
+
ENV['UNICORN_FD'] = srv.fileno.to_s
|
35
|
+
opts = {
|
36
|
+
listeners: [ "#{host}:#{port}" ],
|
37
|
+
stderr_path: err.path,
|
38
|
+
check_client_connection: true,
|
39
|
+
}
|
40
|
+
uni = Unicorn::HttpServer.new(app, opts)
|
41
|
+
uni.start.join
|
42
|
+
end
|
43
|
+
wr.close
|
44
|
+
|
45
|
+
# make sure the server is running, at least
|
46
|
+
client = tcp_socket(host, port)
|
47
|
+
client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
48
|
+
assert client.wait(10), 'never got response from server'
|
49
|
+
res = client.read
|
50
|
+
assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first response'
|
51
|
+
assert_match %r{\r\n\r\n\z}, res, 'got end of response, server is ready'
|
52
|
+
client.close
|
53
|
+
|
54
|
+
# start a slow request...
|
55
|
+
sleeper = tcp_socket(host, port)
|
56
|
+
sleeper.write("GET /sleep HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
57
|
+
|
58
|
+
# and a bunch of aborted ones
|
59
|
+
nr = 100
|
60
|
+
nr.times do |i|
|
61
|
+
client = tcp_socket(host, port)
|
62
|
+
client.write("GET /collections/#{rand(10000)} HTTP/1.1\r\n" \
|
63
|
+
"Host: example.com\r\n\r\n")
|
64
|
+
client.close
|
65
|
+
end
|
66
|
+
sleep_pipe[1].close # wake up the reader in the worker
|
67
|
+
res = sleeper.read
|
68
|
+
assert_match %r{\AHTTP/1\.1 200}, res, 'got part of first sleeper response'
|
69
|
+
assert_match %r{\r\n\r\n\z}, res, 'got end of sleeper response'
|
70
|
+
sleeper.close
|
71
|
+
kpid = pid
|
72
|
+
pid = nil
|
73
|
+
Process.kill(:QUIT, kpid)
|
74
|
+
_, status = Process.waitpid2(kpid)
|
75
|
+
assert status.success?
|
76
|
+
reqs = rd.read.to_i
|
77
|
+
warn "server got #{reqs} requests with #{nr} CCC aborted\n" if $DEBUG
|
78
|
+
assert_operator reqs, :<, nr
|
79
|
+
assert_operator reqs, :>=, 2, 'first 2 requests got through, at least'
|
80
|
+
ensure
|
81
|
+
return if start_pid != $$
|
82
|
+
srv.close if srv
|
83
|
+
if pid
|
84
|
+
Process.kill(:QUIT, pid)
|
85
|
+
_, status = Process.waitpid2(pid)
|
86
|
+
assert status.success?
|
87
|
+
end
|
88
|
+
err.close! if err
|
89
|
+
rd.close if rd
|
90
|
+
end
|
91
|
+
end
|
data/test/unit/test_droplet.rb
CHANGED
@@ -4,7 +4,7 @@ require 'unicorn'
|
|
4
4
|
class TestDroplet < Test::Unit::TestCase
|
5
5
|
def test_create_many_droplets
|
6
6
|
now = Time.now.to_i
|
7
|
-
|
7
|
+
(0..1024).each do |i|
|
8
8
|
droplet = Unicorn::Worker.new(i)
|
9
9
|
assert droplet.respond_to?(:tick)
|
10
10
|
assert_equal 0, droplet.tick
|
@@ -230,6 +230,24 @@ class HttpParserTest < Test::Unit::TestCase
|
|
230
230
|
assert_equal expect, req['HTTP_X_SSL_BULLSHIT']
|
231
231
|
end
|
232
232
|
|
233
|
+
def test_multiline_header_0d0a
|
234
|
+
parser = HttpParser.new
|
235
|
+
parser.buf << "GET / HTTP/1.0\r\n" \
|
236
|
+
"X-Multiline-Header: foo bar\r\n\tcha cha\r\n\tzha zha\r\n\r\n"
|
237
|
+
req = parser.env
|
238
|
+
assert_equal req, parser.parse
|
239
|
+
assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER']
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_multiline_header_0a
|
243
|
+
parser = HttpParser.new
|
244
|
+
parser.buf << "GET / HTTP/1.0\n" \
|
245
|
+
"X-Multiline-Header: foo bar\n\tcha cha\n\tzha zha\n\n"
|
246
|
+
req = parser.env
|
247
|
+
assert_equal req, parser.parse
|
248
|
+
assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER']
|
249
|
+
end
|
250
|
+
|
233
251
|
def test_continuation_eats_leading_spaces
|
234
252
|
parser = HttpParser.new
|
235
253
|
header = "GET / HTTP/1.1\r\n" \
|
@@ -833,22 +851,34 @@ class HttpParserTest < Test::Unit::TestCase
|
|
833
851
|
assert_equal '', parser.env['HTTP_HOST']
|
834
852
|
end
|
835
853
|
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
after = $1.to_i
|
847
|
-
diff = after - before
|
848
|
-
assert(diff < 10000, "memory grew more than 10M: #{diff}")
|
854
|
+
def test_memsize
|
855
|
+
require 'objspace'
|
856
|
+
if ObjectSpace.respond_to?(:memsize_of)
|
857
|
+
n = ObjectSpace.memsize_of(Unicorn::HttpParser.new)
|
858
|
+
assert_kind_of Integer, n
|
859
|
+
# need to update this when 128-bit machines come out
|
860
|
+
# n.b. actual struct size on 64-bit is 56 bytes + 40 bytes for RVALUE
|
861
|
+
# Ruby <= 2.2 objspace did not count the 40-byte RVALUE, 2.3 does.
|
862
|
+
assert_operator n, :<=, 96
|
863
|
+
assert_operator n, :>, 0
|
849
864
|
end
|
850
|
-
|
851
|
-
|
852
|
-
|
865
|
+
rescue LoadError
|
866
|
+
# not all Ruby implementations have objspace
|
867
|
+
end
|
853
868
|
|
869
|
+
def test_dedupe
|
870
|
+
parser = HttpParser.new
|
871
|
+
# n.b. String#freeze optimization doesn't work under modern test-unit
|
872
|
+
exp = -'HTTP_HOST'
|
873
|
+
get = "GET / HTTP/1.1\r\nHost: example.com\r\nHavpbea-fhpxf: true\r\n\r\n"
|
874
|
+
assert parser.add_parse(get)
|
875
|
+
key = parser.env.keys.detect { |k| k == exp }
|
876
|
+
assert_same exp, key
|
877
|
+
|
878
|
+
if RUBY_VERSION.to_r >= 2.6 # 2.6.0-rc1+
|
879
|
+
exp = -'HTTP_HAVPBEA_FHPXF'
|
880
|
+
key = parser.env.keys.detect { |k| k == exp }
|
881
|
+
assert_same exp, key
|
882
|
+
end
|
883
|
+
end if RUBY_VERSION.to_r >= 2.5 && RUBY_ENGINE == 'ruby'
|
854
884
|
end
|
@@ -11,6 +11,20 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
11
11
|
@parser = HttpParser.new
|
12
12
|
end
|
13
13
|
|
14
|
+
# RFC 7230 allows gzip/deflate/compress Transfer-Encoding,
|
15
|
+
# but "chunked" must be last if used
|
16
|
+
def test_is_chunked
|
17
|
+
[ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x|
|
18
|
+
assert_raise(HttpParserError) { HttpParser.is_chunked?(x) }
|
19
|
+
end
|
20
|
+
[ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x|
|
21
|
+
assert HttpParser.is_chunked?(x)
|
22
|
+
end
|
23
|
+
[ 'gzip', 'xhunked', 'xchunked' ].each do |x|
|
24
|
+
assert !HttpParser.is_chunked?(x)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
14
28
|
def test_parser_max_len
|
15
29
|
assert_raises(RangeError) do
|
16
30
|
HttpParser.max_header_len = 0xffffffff + 1
|
@@ -566,6 +580,73 @@ class HttpParserNgTest < Test::Unit::TestCase
|
|
566
580
|
end
|
567
581
|
end
|
568
582
|
|
583
|
+
def test_duplicate_content_length
|
584
|
+
str = "PUT / HTTP/1.1\r\n" \
|
585
|
+
"Content-Length: 1\r\n" \
|
586
|
+
"Content-Length: 9\r\n" \
|
587
|
+
"\r\n"
|
588
|
+
assert_raises(HttpParserError) { @parser.headers({}, str) }
|
589
|
+
end
|
590
|
+
|
591
|
+
def test_chunked_overrides_content_length
|
592
|
+
order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ]
|
593
|
+
%w(a b).each do |x|
|
594
|
+
str = "PUT /#{x} HTTP/1.1\r\n" \
|
595
|
+
"#{order.join("\r\n")}" \
|
596
|
+
"\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n"
|
597
|
+
order.reverse!
|
598
|
+
env = @parser.headers({}, str)
|
599
|
+
assert_nil @parser.content_length
|
600
|
+
assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING']
|
601
|
+
assert_equal '666', env['CONTENT_LENGTH'],
|
602
|
+
'Content-Length logged so the app can log a possible client bug/attack'
|
603
|
+
@parser.filter_body(dst = '', str)
|
604
|
+
assert_equal 'helloworld', dst
|
605
|
+
@parser.parse # handle the non-existent trailer
|
606
|
+
assert @parser.next?
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
def test_chunked_order_good
|
611
|
+
str = "PUT /x HTTP/1.1\r\n" \
|
612
|
+
"Transfer-Encoding: gzip\r\n" \
|
613
|
+
"Transfer-Encoding: chunked\r\n" \
|
614
|
+
"\r\n"
|
615
|
+
env = @parser.headers({}, str)
|
616
|
+
assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING']
|
617
|
+
assert_nil @parser.content_length
|
618
|
+
|
619
|
+
@parser.clear
|
620
|
+
str = "PUT /x HTTP/1.1\r\n" \
|
621
|
+
"Transfer-Encoding: gzip, chunked\r\n" \
|
622
|
+
"\r\n"
|
623
|
+
env = @parser.headers({}, str)
|
624
|
+
assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING']
|
625
|
+
assert_nil @parser.content_length
|
626
|
+
end
|
627
|
+
|
628
|
+
def test_chunked_order_bad
|
629
|
+
str = "PUT /x HTTP/1.1\r\n" \
|
630
|
+
"Transfer-Encoding: chunked\r\n" \
|
631
|
+
"Transfer-Encoding: gzip\r\n" \
|
632
|
+
"\r\n"
|
633
|
+
assert_raise(HttpParserError) { @parser.headers({}, str) }
|
634
|
+
end
|
635
|
+
|
636
|
+
def test_double_chunked
|
637
|
+
str = "PUT /x HTTP/1.1\r\n" \
|
638
|
+
"Transfer-Encoding: chunked\r\n" \
|
639
|
+
"Transfer-Encoding: chunked\r\n" \
|
640
|
+
"\r\n"
|
641
|
+
assert_raise(HttpParserError) { @parser.headers({}, str) }
|
642
|
+
|
643
|
+
@parser.clear
|
644
|
+
str = "PUT /x HTTP/1.1\r\n" \
|
645
|
+
"Transfer-Encoding: chunked,chunked\r\n" \
|
646
|
+
"\r\n"
|
647
|
+
assert_raise(HttpParserError) { @parser.headers({}, str) }
|
648
|
+
end
|
649
|
+
|
569
650
|
def test_backtrace_is_empty
|
570
651
|
begin
|
571
652
|
@parser.headers({}, "AAADFSFDSFD\r\n\r\n")
|
data/test/unit/test_request.rb
CHANGED
@@ -34,7 +34,7 @@ class RequestTest < Test::Unit::TestCase
|
|
34
34
|
assert_equal '', env['REQUEST_PATH']
|
35
35
|
assert_equal '', env['PATH_INFO']
|
36
36
|
assert_equal '*', env['REQUEST_URI']
|
37
|
-
|
37
|
+
assert_kind_of Array, @lint.call(env)
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_absolute_uri_with_query
|
@@ -44,7 +44,7 @@ class RequestTest < Test::Unit::TestCase
|
|
44
44
|
assert_equal '/x', env['REQUEST_PATH']
|
45
45
|
assert_equal '/x', env['PATH_INFO']
|
46
46
|
assert_equal 'y=z', env['QUERY_STRING']
|
47
|
-
|
47
|
+
assert_kind_of Array, @lint.call(env)
|
48
48
|
end
|
49
49
|
|
50
50
|
def test_absolute_uri_with_fragment
|
@@ -55,7 +55,7 @@ class RequestTest < Test::Unit::TestCase
|
|
55
55
|
assert_equal '/x', env['PATH_INFO']
|
56
56
|
assert_equal '', env['QUERY_STRING']
|
57
57
|
assert_equal 'frag', env['FRAGMENT']
|
58
|
-
|
58
|
+
assert_kind_of Array, @lint.call(env)
|
59
59
|
end
|
60
60
|
|
61
61
|
def test_absolute_uri_with_query_and_fragment
|
@@ -66,7 +66,7 @@ class RequestTest < Test::Unit::TestCase
|
|
66
66
|
assert_equal '/x', env['PATH_INFO']
|
67
67
|
assert_equal 'a=b', env['QUERY_STRING']
|
68
68
|
assert_equal 'frag', env['FRAGMENT']
|
69
|
-
|
69
|
+
assert_kind_of Array, @lint.call(env)
|
70
70
|
end
|
71
71
|
|
72
72
|
def test_absolute_uri_unsupported_schemes
|
@@ -83,7 +83,7 @@ class RequestTest < Test::Unit::TestCase
|
|
83
83
|
"Host: foo\r\n\r\n")
|
84
84
|
env = @request.read(client)
|
85
85
|
assert_equal "https", env['rack.url_scheme']
|
86
|
-
|
86
|
+
assert_kind_of Array, @lint.call(env)
|
87
87
|
end
|
88
88
|
|
89
89
|
def test_x_forwarded_proto_http
|
@@ -92,7 +92,7 @@ class RequestTest < Test::Unit::TestCase
|
|
92
92
|
"Host: foo\r\n\r\n")
|
93
93
|
env = @request.read(client)
|
94
94
|
assert_equal "http", env['rack.url_scheme']
|
95
|
-
|
95
|
+
assert_kind_of Array, @lint.call(env)
|
96
96
|
end
|
97
97
|
|
98
98
|
def test_x_forwarded_proto_invalid
|
@@ -101,7 +101,7 @@ class RequestTest < Test::Unit::TestCase
|
|
101
101
|
"Host: foo\r\n\r\n")
|
102
102
|
env = @request.read(client)
|
103
103
|
assert_equal "http", env['rack.url_scheme']
|
104
|
-
|
104
|
+
assert_kind_of Array, @lint.call(env)
|
105
105
|
end
|
106
106
|
|
107
107
|
def test_rack_lint_get
|
@@ -109,7 +109,7 @@ class RequestTest < Test::Unit::TestCase
|
|
109
109
|
env = @request.read(client)
|
110
110
|
assert_equal "http", env['rack.url_scheme']
|
111
111
|
assert_equal '127.0.0.1', env['REMOTE_ADDR']
|
112
|
-
|
112
|
+
assert_kind_of Array, @lint.call(env)
|
113
113
|
end
|
114
114
|
|
115
115
|
def test_no_content_stringio
|
@@ -143,7 +143,7 @@ class RequestTest < Test::Unit::TestCase
|
|
143
143
|
"abcde")
|
144
144
|
env = @request.read(client)
|
145
145
|
assert ! env.include?(:http_body)
|
146
|
-
|
146
|
+
assert_kind_of Array, @lint.call(env)
|
147
147
|
end
|
148
148
|
|
149
149
|
def test_rack_lint_big_put
|
@@ -177,6 +177,6 @@ class RequestTest < Test::Unit::TestCase
|
|
177
177
|
}
|
178
178
|
assert_nil env['rack.input'].read(bs)
|
179
179
|
env['rack.input'].rewind
|
180
|
-
|
180
|
+
assert_kind_of Array, @lint.call(env)
|
181
181
|
end
|
182
182
|
end
|