boourns-unicorn 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. data/.CHANGELOG.old +25 -0
  2. data/.document +29 -0
  3. data/.gitignore +24 -0
  4. data/.mailmap +26 -0
  5. data/.wrongdoc.yml +10 -0
  6. data/Application_Timeouts +77 -0
  7. data/CONTRIBUTORS +35 -0
  8. data/COPYING +674 -0
  9. data/DESIGN +97 -0
  10. data/Documentation/.gitignore +5 -0
  11. data/Documentation/GNUmakefile +30 -0
  12. data/Documentation/unicorn.1.txt +174 -0
  13. data/Documentation/unicorn_rails.1.txt +175 -0
  14. data/FAQ +53 -0
  15. data/GIT-VERSION-GEN +40 -0
  16. data/GNUmakefile +267 -0
  17. data/HACKING +134 -0
  18. data/ISSUES +36 -0
  19. data/KNOWN_ISSUES +79 -0
  20. data/LICENSE +64 -0
  21. data/Links +56 -0
  22. data/PHILOSOPHY +145 -0
  23. data/README +149 -0
  24. data/Rakefile +97 -0
  25. data/SIGNALS +114 -0
  26. data/Sandbox +96 -0
  27. data/TODO +5 -0
  28. data/TUNING +98 -0
  29. data/bin/unicorn +121 -0
  30. data/bin/unicorn_rails +209 -0
  31. data/examples/big_app_gc.rb +2 -0
  32. data/examples/echo.ru +27 -0
  33. data/examples/git.ru +13 -0
  34. data/examples/init.sh +74 -0
  35. data/examples/logger_mp_safe.rb +25 -0
  36. data/examples/logrotate.conf +29 -0
  37. data/examples/nginx.conf +156 -0
  38. data/examples/unicorn.conf.minimal.rb +13 -0
  39. data/examples/unicorn.conf.rb +94 -0
  40. data/ext/unicorn_http/CFLAGS +13 -0
  41. data/ext/unicorn_http/c_util.h +124 -0
  42. data/ext/unicorn_http/common_field_optimization.h +111 -0
  43. data/ext/unicorn_http/ext_help.h +86 -0
  44. data/ext/unicorn_http/extconf.rb +10 -0
  45. data/ext/unicorn_http/global_variables.h +97 -0
  46. data/ext/unicorn_http/httpdate.c +82 -0
  47. data/ext/unicorn_http/unicorn_http.rl +1036 -0
  48. data/ext/unicorn_http/unicorn_http_common.rl +76 -0
  49. data/lib/unicorn.rb +107 -0
  50. data/lib/unicorn/app/exec_cgi.rb +154 -0
  51. data/lib/unicorn/app/inetd.rb +109 -0
  52. data/lib/unicorn/app/old_rails.rb +35 -0
  53. data/lib/unicorn/app/old_rails/static.rb +59 -0
  54. data/lib/unicorn/cgi_wrapper.rb +147 -0
  55. data/lib/unicorn/configurator.rb +630 -0
  56. data/lib/unicorn/const.rb +40 -0
  57. data/lib/unicorn/http_request.rb +83 -0
  58. data/lib/unicorn/http_response.rb +45 -0
  59. data/lib/unicorn/http_server.rb +755 -0
  60. data/lib/unicorn/launcher.rb +62 -0
  61. data/lib/unicorn/oob_gc.rb +71 -0
  62. data/lib/unicorn/preread_input.rb +33 -0
  63. data/lib/unicorn/socket_helper.rb +208 -0
  64. data/lib/unicorn/ssl_client.rb +11 -0
  65. data/lib/unicorn/ssl_configurator.rb +104 -0
  66. data/lib/unicorn/ssl_server.rb +42 -0
  67. data/lib/unicorn/stream_input.rb +149 -0
  68. data/lib/unicorn/tee_input.rb +126 -0
  69. data/lib/unicorn/tmpio.rb +29 -0
  70. data/lib/unicorn/util.rb +69 -0
  71. data/lib/unicorn/worker.rb +88 -0
  72. data/local.mk.sample +59 -0
  73. data/script/isolate_for_tests +32 -0
  74. data/setup.rb +1586 -0
  75. data/t/.gitignore +5 -0
  76. data/t/GNUmakefile +82 -0
  77. data/t/README +42 -0
  78. data/t/bin/content-md5-put +36 -0
  79. data/t/bin/sha1sum.rb +17 -0
  80. data/t/bin/unused_listen +40 -0
  81. data/t/bin/utee +12 -0
  82. data/t/broken-app.ru +12 -0
  83. data/t/detach.ru +11 -0
  84. data/t/env.ru +3 -0
  85. data/t/heartbeat-timeout.ru +12 -0
  86. data/t/listener_names.ru +4 -0
  87. data/t/my-tap-lib.sh +201 -0
  88. data/t/oob_gc.ru +21 -0
  89. data/t/oob_gc_path.ru +21 -0
  90. data/t/pid.ru +3 -0
  91. data/t/preread_input.ru +17 -0
  92. data/t/rack-input-tests.ru +21 -0
  93. data/t/sslgen.sh +71 -0
  94. data/t/t0000-http-basic.sh +50 -0
  95. data/t/t0001-reload-bad-config.sh +53 -0
  96. data/t/t0002-config-conflict.sh +49 -0
  97. data/t/t0002-parser-error.sh +94 -0
  98. data/t/t0003-working_directory.sh +51 -0
  99. data/t/t0004-heartbeat-timeout.sh +69 -0
  100. data/t/t0004-working_directory_broken.sh +24 -0
  101. data/t/t0005-working_directory_app.rb.sh +37 -0
  102. data/t/t0006-reopen-logs.sh +83 -0
  103. data/t/t0006.ru +13 -0
  104. data/t/t0007-working_directory_no_embed_cli.sh +44 -0
  105. data/t/t0008-back_out_of_upgrade.sh +110 -0
  106. data/t/t0009-broken-app.sh +56 -0
  107. data/t/t0009-winch_ttin.sh +59 -0
  108. data/t/t0010-reap-logging.sh +55 -0
  109. data/t/t0011-active-unix-socket.sh +79 -0
  110. data/t/t0012-reload-empty-config.sh +85 -0
  111. data/t/t0013-rewindable-input-false.sh +24 -0
  112. data/t/t0013.ru +12 -0
  113. data/t/t0014-rewindable-input-true.sh +24 -0
  114. data/t/t0014.ru +12 -0
  115. data/t/t0015-configurator-internals.sh +25 -0
  116. data/t/t0016-trust-x-forwarded-false.sh +30 -0
  117. data/t/t0017-trust-x-forwarded-true.sh +30 -0
  118. data/t/t0018-write-on-close.sh +23 -0
  119. data/t/t0019-max_header_len.sh +49 -0
  120. data/t/t0020-at_exit-handler.sh +49 -0
  121. data/t/t0021-process_detach.sh +29 -0
  122. data/t/t0022-listener_names-preload_app.sh +32 -0
  123. data/t/t0100-rack-input-tests.sh +124 -0
  124. data/t/t0116-client_body_buffer_size.sh +80 -0
  125. data/t/t0116.ru +16 -0
  126. data/t/t0600-https-server-basic.sh +48 -0
  127. data/t/t9000-preread-input.sh +48 -0
  128. data/t/t9001-oob_gc.sh +47 -0
  129. data/t/t9002-oob_gc-path.sh +75 -0
  130. data/t/test-lib.sh +113 -0
  131. data/t/write-on-close.ru +11 -0
  132. data/test/aggregate.rb +15 -0
  133. data/test/benchmark/README +50 -0
  134. data/test/benchmark/dd.ru +18 -0
  135. data/test/benchmark/stack.ru +8 -0
  136. data/test/exec/README +5 -0
  137. data/test/exec/test_exec.rb +1041 -0
  138. data/test/test_helper.rb +300 -0
  139. data/test/unit/test_configurator.rb +158 -0
  140. data/test/unit/test_droplet.rb +28 -0
  141. data/test/unit/test_http_parser.rb +860 -0
  142. data/test/unit/test_http_parser_ng.rb +716 -0
  143. data/test/unit/test_http_parser_xftrust.rb +38 -0
  144. data/test/unit/test_request.rb +197 -0
  145. data/test/unit/test_response.rb +99 -0
  146. data/test/unit/test_server.rb +289 -0
  147. data/test/unit/test_signals.rb +207 -0
  148. data/test/unit/test_sni_hostnames.rb +47 -0
  149. data/test/unit/test_socket_helper.rb +192 -0
  150. data/test/unit/test_stream_input.rb +204 -0
  151. data/test/unit/test_tee_input.rb +296 -0
  152. data/test/unit/test_upload.rb +306 -0
  153. data/test/unit/test_util.rb +99 -0
  154. data/unicorn.gemspec +44 -0
  155. metadata +333 -0
@@ -0,0 +1,48 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+ t_plan 9 "PrereadInput middleware tests"
4
+
5
+ t_begin "setup and start" && {
6
+ random_blob_sha1=$(rsha1 < random_blob)
7
+ unicorn_setup
8
+ unicorn -D -c $unicorn_config preread_input.ru
9
+ unicorn_wait_start
10
+ }
11
+
12
+ t_begin "single identity request" && {
13
+ curl -sSf -T random_blob http://$listen/ > $tmp
14
+ }
15
+
16
+ t_begin "sha1 matches" && {
17
+ test x"$(cat $tmp)" = x"$random_blob_sha1"
18
+ }
19
+
20
+ t_begin "single chunked request" && {
21
+ curl -sSf -T- < random_blob http://$listen/ > $tmp
22
+ }
23
+
24
+ t_begin "sha1 matches" && {
25
+ test x"$(cat $tmp)" = x"$random_blob_sha1"
26
+ }
27
+
28
+ t_begin "app only dispatched twice" && {
29
+ test 2 -eq "$(grep 'app dispatch:' < $r_err | wc -l )"
30
+ }
31
+
32
+ t_begin "aborted chunked request" && {
33
+ rm -f $tmp
34
+ curl -sSf -T- < $fifo http://$listen/ > $tmp &
35
+ curl_pid=$!
36
+ kill -9 $curl_pid
37
+ wait
38
+ }
39
+
40
+ t_begin "app only dispatched twice" && {
41
+ test 2 -eq "$(grep 'app dispatch:' < $r_err | wc -l )"
42
+ }
43
+
44
+ t_begin "killing succeeds" && {
45
+ kill -QUIT $unicorn_pid
46
+ }
47
+
48
+ t_done
data/t/t9001-oob_gc.sh ADDED
@@ -0,0 +1,47 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+ t_plan 9 "OobGC test"
4
+
5
+ t_begin "setup and start" && {
6
+ unicorn_setup
7
+ unicorn -D -c $unicorn_config oob_gc.ru
8
+ unicorn_wait_start
9
+ }
10
+
11
+ t_begin "test default interval (4 requests)" && {
12
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
13
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
14
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
15
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
16
+ }
17
+
18
+ t_begin "GC starting-request returns immediately" && {
19
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
20
+ }
21
+
22
+ t_begin "GC is started after 5 requests" && {
23
+ test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp)
24
+ }
25
+
26
+ t_begin "reset GC" && {
27
+ test xfalse = x$(curl -vsSf -X POST http://$listen/gc_reset 2>> $tmp)
28
+ }
29
+
30
+ t_begin "test default interval again (3 requests)" && {
31
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
32
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
33
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
34
+ }
35
+
36
+ t_begin "GC is started after 5 requests" && {
37
+ test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp)
38
+ }
39
+
40
+ t_begin "killing succeeds" && {
41
+ kill -QUIT $unicorn_pid
42
+ }
43
+
44
+ t_begin "check_stderr" && check_stderr
45
+ dbgcat r_err
46
+
47
+ t_done
@@ -0,0 +1,75 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+ t_plan 12 "OobGC test with limited path"
4
+
5
+ t_begin "setup and start" && {
6
+ unicorn_setup
7
+ unicorn -D -c $unicorn_config oob_gc_path.ru
8
+ unicorn_wait_start
9
+ }
10
+
11
+ t_begin "test default is noop" && {
12
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
13
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
14
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
15
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
16
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
17
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
18
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
19
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
20
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
21
+ }
22
+
23
+ t_begin "4 bad requests to bump counter" && {
24
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
25
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
26
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
27
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
28
+ }
29
+
30
+ t_begin "GC-starting request returns immediately" && {
31
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
32
+ }
33
+
34
+ t_begin "GC was started after 5 requests" && {
35
+ test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp)
36
+ }
37
+
38
+ t_begin "reset GC" && {
39
+ test xfalse = x$(curl -vsSf -X POST http://$listen/gc_reset 2>> $tmp)
40
+ }
41
+
42
+ t_begin "test default is noop" && {
43
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
44
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
45
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
46
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
47
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
48
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
49
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
50
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
51
+ test xfalse = x$(curl -vsSf http://$listen/ 2>> $tmp)
52
+ }
53
+
54
+ t_begin "4 bad requests to bump counter" && {
55
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
56
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
57
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
58
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
59
+ }
60
+
61
+ t_begin "GC-starting request returns immediately" && {
62
+ test xfalse = x$(curl -vsSf http://$listen/BAD 2>> $tmp)
63
+ }
64
+
65
+ t_begin "GC was started after 5 requests" && {
66
+ test xtrue = x$(curl -vsSf http://$listen/ 2>> $tmp)
67
+ }
68
+
69
+ t_begin "killing succeeds" && {
70
+ kill -QUIT $unicorn_pid
71
+ }
72
+
73
+ t_begin "check_stderr" && check_stderr
74
+
75
+ t_done
data/t/test-lib.sh ADDED
@@ -0,0 +1,113 @@
1
+ #!/bin/sh
2
+ # Copyright (c) 2009 Rainbows! hackers
3
+ # Copyright (c) 2010 Unicorn hackers
4
+ . ./my-tap-lib.sh
5
+
6
+ set +u
7
+
8
+ # sometimes we rely on http_proxy to avoid wasting bandwidth with Isolate
9
+ # and multiple Ruby versions
10
+ NO_PROXY=${UNICORN_TEST_ADDR-127.0.0.1}
11
+ export NO_PROXY
12
+
13
+ set -e
14
+ RUBY="${RUBY-ruby}"
15
+ RUBY_VERSION=${RUBY_VERSION-$($RUBY -e 'puts RUBY_VERSION')}
16
+ RUBY_ENGINE=${RUBY_ENGINE-$($RUBY -e 'puts((RUBY_ENGINE rescue "ruby"))')}
17
+ t_pfx=$PWD/trash/$T-$RUBY_ENGINE-$RUBY_VERSION
18
+ set -u
19
+
20
+ PATH=$PWD/bin:$PATH
21
+ export PATH
22
+
23
+ test -x $PWD/bin/unused_listen || die "must be run in 't' directory"
24
+
25
+ wait_for_pid () {
26
+ path="$1"
27
+ nr=30
28
+ while ! test -s "$path" && test $nr -gt 0
29
+ do
30
+ nr=$(($nr - 1))
31
+ sleep 1
32
+ done
33
+ }
34
+
35
+ # given a list of variable names, create temporary files and assign
36
+ # the pathnames to those variables
37
+ rtmpfiles () {
38
+ for id in "$@"
39
+ do
40
+ name=$id
41
+
42
+ case $name in
43
+ *fifo)
44
+ _tmp=$t_pfx.$id
45
+ eval "$id=$_tmp"
46
+ rm -f $_tmp
47
+ mkfifo $_tmp
48
+ T_RM_LIST="$T_RM_LIST $_tmp"
49
+ ;;
50
+ *socket)
51
+ _tmp="$(mktemp -t $id.$$.XXXXXXXX)"
52
+ if test $(printf "$_tmp" |wc -c) -gt 108
53
+ then
54
+ echo >&2 "$_tmp too long, tests may fail"
55
+ echo >&2 "Try to set TMPDIR to a shorter path"
56
+ fi
57
+ eval "$id=$_tmp"
58
+ rm -f $_tmp
59
+ T_RM_LIST="$T_RM_LIST $_tmp"
60
+ ;;
61
+ *)
62
+ _tmp=$t_pfx.$id
63
+ eval "$id=$_tmp"
64
+ > $_tmp
65
+ T_OK_RM_LIST="$T_OK_RM_LIST $_tmp"
66
+ ;;
67
+ esac
68
+ done
69
+ }
70
+
71
+ dbgcat () {
72
+ id=$1
73
+ eval '_file=$'$id
74
+ echo "==> $id <=="
75
+ sed -e "s/^/$id:/" < $_file
76
+ }
77
+
78
+ check_stderr () {
79
+ set +u
80
+ _r_err=${1-${r_err}}
81
+ set -u
82
+ if grep -v $T $_r_err | grep -i Error
83
+ then
84
+ die "Errors found in $_r_err"
85
+ elif grep SIGKILL $_r_err
86
+ then
87
+ die "SIGKILL found in $_r_err"
88
+ fi
89
+ }
90
+
91
+ # unicorn_setup
92
+ unicorn_setup () {
93
+ eval $(unused_listen)
94
+ port=$(expr $listen : '[^:]*:\([0-9]\+\)')
95
+ host=$(expr $listen : '\([^:]*\):[0-9]\+')
96
+
97
+ rtmpfiles unicorn_config pid r_err r_out fifo tmp ok
98
+ cat > $unicorn_config <<EOF
99
+ listen "$listen"
100
+ pid "$pid"
101
+ stderr_path "$r_err"
102
+ stdout_path "$r_out"
103
+ EOF
104
+ }
105
+
106
+ unicorn_wait_start () {
107
+ # no need to play tricks with FIFOs since we got "ready_pipe" now
108
+ unicorn_pid=$(cat $pid)
109
+ }
110
+
111
+ rsha1 () {
112
+ sha1sum.rb
113
+ }
@@ -0,0 +1,11 @@
1
+ class WriteOnClose
2
+ def each(&block)
3
+ @callback = block
4
+ end
5
+
6
+ def close
7
+ @callback.call "7\r\nGoodbye\r\n0\r\n\r\n"
8
+ end
9
+ end
10
+ use Rack::ContentType, "text/plain"
11
+ run(lambda { |_| [ 200, [%w(Transfer-Encoding chunked)], WriteOnClose.new ] })
data/test/aggregate.rb ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/ruby -n
2
+ # -*- encoding: binary -*-
3
+
4
+ BEGIN { $tests = $assertions = $failures = $errors = 0 }
5
+
6
+ $_ =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/ or next
7
+ $tests += $1.to_i
8
+ $assertions += $2.to_i
9
+ $failures += $3.to_i
10
+ $errors += $4.to_i
11
+
12
+ END {
13
+ printf("\n%d tests, %d assertions, %d failures, %d errors\n",
14
+ $tests, $assertions, $failures, $errors)
15
+ }
@@ -0,0 +1,50 @@
1
+ = Performance
2
+
3
+ Unicorn is pretty fast, and we want it to get faster. Unicorn strives
4
+ to get HTTP requests to your application and write HTTP responses back
5
+ as quickly as possible. Unicorn does not do any background processing
6
+ while your app runs, so your app will get all the CPU time provided to
7
+ it by your OS kernel.
8
+
9
+ A gentle reminder: Unicorn is NOT for serving clients over slow network
10
+ connections. Use nginx (or something similar) to complement Unicorn if
11
+ you have slow clients.
12
+
13
+ == dd.ru
14
+
15
+ This is a pure I/O benchmark. In the context of Unicorn, this is the
16
+ only one that matters. It is a standard rackup-compatible .ru file and
17
+ may be used with other Rack-compatible servers.
18
+
19
+ unicorn -E none dd.ru
20
+
21
+ You can change the size and number of chunks in the response with
22
+ the "bs" and "count" environment variables. The following command
23
+ will cause dd.ru to return 4 chunks of 16384 bytes each, leading to
24
+ 65536 byte response:
25
+
26
+ bs=16384 count=4 unicorn -E none dd.ru
27
+
28
+ Or if you want to add logging (small performance impact):
29
+
30
+ unicorn -E deployment dd.ru
31
+
32
+ Eric runs then runs clients on a LAN it in several different ways:
33
+
34
+ client@host1 -> unicorn@host1(tcp)
35
+ client@host2 -> unicorn@host1(tcp)
36
+ client@host3 -> nginx@host1 -> unicorn@host1(tcp)
37
+ client@host3 -> nginx@host1 -> unicorn@host1(unix)
38
+ client@host3 -> nginx@host2 -> unicorn@host1(tcp)
39
+
40
+ The benchmark client is usually httperf.
41
+
42
+ Another gentle reminder: performance with slow networks/clients
43
+ is NOT our problem. That is the job of nginx (or similar).
44
+
45
+ == Contributors
46
+
47
+ This directory is maintained independently in the "benchmark" branch
48
+ based against v0.1.0. Only changes to this directory (test/benchmarks)
49
+ are committed to this branch although the master branch may merge this
50
+ branch occassionaly.
@@ -0,0 +1,18 @@
1
+ # This benchmark is the simplest test of the I/O facilities in
2
+ # unicorn. It is meant to return a fixed-sized blob to test
3
+ # the performance of things in Unicorn, _NOT_ the app.
4
+ #
5
+ # Adjusting this benchmark is done via the "bs" (byte size) and "count"
6
+ # environment variables. "count" designates the count of elements of
7
+ # "bs" length in the Rack response body. The defaults are bs=4096, count=1
8
+ # to return one 4096-byte chunk.
9
+ bs = ENV['bs'] ? ENV['bs'].to_i : 4096
10
+ count = ENV['count'] ? ENV['count'].to_i : 1
11
+ slice = (' ' * bs).freeze
12
+ body = (1..count).map { slice }.freeze
13
+ hdr = {
14
+ 'Content-Length' => (bs * count).to_s.freeze,
15
+ 'Content-Type' => 'text/plain'.freeze
16
+ }.freeze
17
+ response = [ 200, hdr, body ].freeze
18
+ run(lambda { |env| response })
@@ -0,0 +1,8 @@
1
+ run(lambda { |env|
2
+ body = "#{caller.size}\n"
3
+ h = {
4
+ "Content-Length" => body.size.to_s,
5
+ "Content-Type" => "text/plain",
6
+ }
7
+ [ 200, h, [ body ] ]
8
+ })
data/test/exec/README ADDED
@@ -0,0 +1,5 @@
1
+ These tests require the "unicorn" executable script to be installed in
2
+ PATH and rack being directly "require"-able ("rubygems" will not be
3
+ loaded for you). The tester is responsible for setting up RUBYLIB and
4
+ PATH environment variables (or running tests via GNU Make instead of
5
+ Rake).
@@ -0,0 +1,1041 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # Copyright (c) 2009 Eric Wong
4
+ FLOCK_PATH = File.expand_path(__FILE__)
5
+ require 'test/test_helper'
6
+
7
+ do_test = true
8
+ $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
9
+ redirect_test_io do
10
+ do_test = system($unicorn_bin, '-v')
11
+ end
12
+
13
+ unless do_test
14
+ warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
15
+ "skipping this test"
16
+ end
17
+
18
+ unless try_require('rack')
19
+ warn "Unable to load Rack, skipping this test"
20
+ do_test = false
21
+ end
22
+
23
+ class ExecTest < Test::Unit::TestCase
24
+ trap(:QUIT, 'IGNORE')
25
+
26
+ HI = <<-EOS
27
+ use Rack::ContentLength
28
+ run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
29
+ EOS
30
+
31
+ SHOW_RACK_ENV = <<-EOS
32
+ use Rack::ContentLength
33
+ run proc { |env|
34
+ [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
35
+ }
36
+ EOS
37
+
38
+ HELLO = <<-EOS
39
+ class Hello
40
+ def call(env)
41
+ [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
42
+ end
43
+ end
44
+ EOS
45
+
46
+ COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
47
+
48
+ HEAVY_CFG = <<-EOS
49
+ worker_processes 4
50
+ timeout 30
51
+ logger Logger.new('#{COMMON_TMP.path}')
52
+ before_fork do |server, worker|
53
+ server.logger.info "before_fork: worker=\#{worker.nr}"
54
+ end
55
+ EOS
56
+
57
+ WORKING_DIRECTORY_CHECK_RU = <<-EOS
58
+ use Rack::ContentLength
59
+ run lambda { |env|
60
+ pwd = ENV['PWD']
61
+ a = ::File.stat(pwd)
62
+ b = ::File.stat(Dir.pwd)
63
+ if (a.ino == b.ino && a.dev == b.dev)
64
+ [ 200, { 'Content-Type' => 'text/plain' }, [ pwd ] ]
65
+ else
66
+ [ 404, { 'Content-Type' => 'text/plain' }, [] ]
67
+ end
68
+ }
69
+ EOS
70
+
71
+ def setup
72
+ @pwd = Dir.pwd
73
+ @tmpfile = Tempfile.new('unicorn_exec_test')
74
+ @tmpdir = @tmpfile.path
75
+ @tmpfile.close!
76
+ Dir.mkdir(@tmpdir)
77
+ Dir.chdir(@tmpdir)
78
+ @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
79
+ @port = unused_port(@addr)
80
+ @sockets = []
81
+ @start_pid = $$
82
+ end
83
+
84
+ def teardown
85
+ return if @start_pid != $$
86
+ Dir.chdir(@pwd)
87
+ FileUtils.rmtree(@tmpdir)
88
+ @sockets.each { |path| File.unlink(path) rescue nil }
89
+ loop do
90
+ Process.kill('-QUIT', 0)
91
+ begin
92
+ Process.waitpid(-1, Process::WNOHANG) or break
93
+ rescue Errno::ECHILD
94
+ break
95
+ end
96
+ end
97
+ end
98
+
99
+ def test_working_directory_rel_path_config_file
100
+ other = Tempfile.new('unicorn.wd')
101
+ File.unlink(other.path)
102
+ Dir.mkdir(other.path)
103
+ File.open("config.ru", "wb") do |fp|
104
+ fp.syswrite WORKING_DIRECTORY_CHECK_RU
105
+ end
106
+ FileUtils.cp("config.ru", other.path + "/config.ru")
107
+ Dir.chdir(@tmpdir)
108
+
109
+ tmp = File.open('unicorn.config', 'wb')
110
+ tmp.syswrite <<EOF
111
+ working_directory '#@tmpdir'
112
+ listen '#@addr:#@port'
113
+ EOF
114
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
115
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
116
+ results = hit(["http://#@addr:#@port/"])
117
+ assert_equal @tmpdir, results.first
118
+ File.truncate("test_stderr.#{pid}.log", 0)
119
+
120
+ tmp.sysseek(0)
121
+ tmp.truncate(0)
122
+ tmp.syswrite <<EOF
123
+ working_directory '#{other.path}'
124
+ listen '#@addr:#@port'
125
+ EOF
126
+
127
+ Process.kill(:HUP, pid)
128
+ lines = []
129
+ re = /config_file=(.+) would not be accessible in working_directory=(.+)/
130
+ until lines.grep(re)
131
+ sleep 0.1
132
+ lines = File.readlines("test_stderr.#{pid}.log")
133
+ end
134
+
135
+ File.truncate("test_stderr.#{pid}.log", 0)
136
+ FileUtils.cp('unicorn.config', other.path + "/unicorn.config")
137
+ Process.kill(:HUP, pid)
138
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
139
+ results = hit(["http://#@addr:#@port/"])
140
+ assert_equal other.path, results.first
141
+
142
+ Process.kill(:QUIT, pid)
143
+ ensure
144
+ FileUtils.rmtree(other.path)
145
+ end
146
+
147
+ def test_working_directory
148
+ other = Tempfile.new('unicorn.wd')
149
+ File.unlink(other.path)
150
+ Dir.mkdir(other.path)
151
+ File.open("config.ru", "wb") do |fp|
152
+ fp.syswrite WORKING_DIRECTORY_CHECK_RU
153
+ end
154
+ FileUtils.cp("config.ru", other.path + "/config.ru")
155
+ tmp = Tempfile.new('unicorn.config')
156
+ tmp.syswrite <<EOF
157
+ working_directory '#@tmpdir'
158
+ listen '#@addr:#@port'
159
+ EOF
160
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
161
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
162
+ results = hit(["http://#@addr:#@port/"])
163
+ assert_equal @tmpdir, results.first
164
+ File.truncate("test_stderr.#{pid}.log", 0)
165
+
166
+ tmp.sysseek(0)
167
+ tmp.truncate(0)
168
+ tmp.syswrite <<EOF
169
+ working_directory '#{other.path}'
170
+ listen '#@addr:#@port'
171
+ EOF
172
+
173
+ Process.kill(:HUP, pid)
174
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
175
+ results = hit(["http://#@addr:#@port/"])
176
+ assert_equal other.path, results.first
177
+
178
+ Process.kill(:QUIT, pid)
179
+ ensure
180
+ FileUtils.rmtree(other.path)
181
+ end
182
+
183
+ def test_working_directory_controls_relative_paths
184
+ other = Tempfile.new('unicorn.wd')
185
+ File.unlink(other.path)
186
+ Dir.mkdir(other.path)
187
+ File.open("config.ru", "wb") do |fp|
188
+ fp.syswrite WORKING_DIRECTORY_CHECK_RU
189
+ end
190
+ FileUtils.cp("config.ru", other.path + "/config.ru")
191
+ system('mkfifo', "#{other.path}/fifo")
192
+ tmp = Tempfile.new('unicorn.config')
193
+ tmp.syswrite <<EOF
194
+ pid "pid_file_here"
195
+ stderr_path "stderr_log_here"
196
+ stdout_path "stdout_log_here"
197
+ working_directory '#{other.path}'
198
+ listen '#@addr:#@port'
199
+ after_fork do |server, worker|
200
+ File.open("fifo", "wb").close
201
+ end
202
+ EOF
203
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
204
+ File.open("#{other.path}/fifo", "rb").close
205
+
206
+ assert ! File.exist?("stderr_log_here")
207
+ assert ! File.exist?("stdout_log_here")
208
+ assert ! File.exist?("pid_file_here")
209
+
210
+ assert ! File.exist?("#@tmpdir/stderr_log_here")
211
+ assert ! File.exist?("#@tmpdir/stdout_log_here")
212
+ assert ! File.exist?("#@tmpdir/pid_file_here")
213
+
214
+ assert File.exist?("#{other.path}/pid_file_here")
215
+ assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
216
+ assert File.exist?("#{other.path}/stderr_log_here")
217
+ assert File.exist?("#{other.path}/stdout_log_here")
218
+ wait_master_ready("#{other.path}/stderr_log_here")
219
+
220
+ Process.kill(:QUIT, pid)
221
+ ensure
222
+ FileUtils.rmtree(other.path)
223
+ end
224
+
225
+
226
+ def test_exit_signals
227
+ %w(INT TERM QUIT).each do |sig|
228
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
229
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
230
+ wait_master_ready("test_stderr.#{pid}.log")
231
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
232
+
233
+ Process.kill(sig, pid)
234
+ pid, status = Process.waitpid2(pid)
235
+
236
+ reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
237
+ assert_equal 1, reaped.size
238
+ assert status.exited?
239
+ end
240
+ end
241
+
242
+ def test_basic
243
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
244
+ pid = fork do
245
+ redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
246
+ end
247
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
248
+ assert_equal String, results[0].class
249
+ assert_shutdown(pid)
250
+ end
251
+
252
+ def test_rack_env_unset
253
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
254
+ pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
255
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
256
+ assert_equal "development", results.first
257
+ assert_shutdown(pid)
258
+ end
259
+
260
+ def test_rack_env_cli_set
261
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
262
+ pid = fork {
263
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
264
+ }
265
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
266
+ assert_equal "asdf", results.first
267
+ assert_shutdown(pid)
268
+ end
269
+
270
+ def test_rack_env_ENV_set
271
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
272
+ pid = fork {
273
+ ENV["RACK_ENV"] = "foobar"
274
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
275
+ }
276
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
277
+ assert_equal "foobar", results.first
278
+ assert_shutdown(pid)
279
+ end
280
+
281
+ def test_rack_env_cli_override_ENV
282
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
283
+ pid = fork {
284
+ ENV["RACK_ENV"] = "foobar"
285
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
286
+ }
287
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
288
+ assert_equal "asdf", results.first
289
+ assert_shutdown(pid)
290
+ end
291
+
292
+ def test_ttin_ttou
293
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
294
+ pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
295
+ log = "test_stderr.#{pid}.log"
296
+ wait_master_ready(log)
297
+ [ 2, 3].each { |i|
298
+ Process.kill(:TTIN, pid)
299
+ wait_workers_ready(log, i)
300
+ }
301
+ File.truncate(log, 0)
302
+ reaped = nil
303
+ [ 2, 1, 0].each { |i|
304
+ Process.kill(:TTOU, pid)
305
+ DEFAULT_TRIES.times {
306
+ sleep DEFAULT_RES
307
+ reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
308
+ break if reaped.size == 1
309
+ }
310
+ assert_equal 1, reaped.size
311
+ }
312
+ end
313
+
314
+ def test_help
315
+ redirect_test_io do
316
+ assert(system($unicorn_bin, "-h"), "help text returns true")
317
+ end
318
+ assert_equal 0, File.stat("test_stderr.#$$.log").size
319
+ assert_not_equal 0, File.stat("test_stdout.#$$.log").size
320
+ lines = File.readlines("test_stdout.#$$.log")
321
+
322
+ # Be considerate of the on-call technician working from their
323
+ # mobile phone or netbook on a slow connection :)
324
+ assert lines.size <= 24, "help height fits in an ANSI terminal window"
325
+ lines.each do |line|
326
+ assert line.size <= 80, "help width fits in an ANSI terminal window"
327
+ end
328
+ end
329
+
330
+ def test_broken_reexec_config
331
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
332
+ pid_file = "#{@tmpdir}/test.pid"
333
+ old_file = "#{pid_file}.oldbin"
334
+ ucfg = Tempfile.new('unicorn_test_config')
335
+ ucfg.syswrite("listen %(#@addr:#@port)\n")
336
+ ucfg.syswrite("pid %(#{pid_file})\n")
337
+ ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
338
+ pid = xfork do
339
+ redirect_test_io do
340
+ exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
341
+ end
342
+ end
343
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
344
+ assert_equal String, results[0].class
345
+
346
+ wait_for_file(pid_file)
347
+ Process.waitpid(pid)
348
+ Process.kill(:USR2, File.read(pid_file).to_i)
349
+ wait_for_file(old_file)
350
+ wait_for_file(pid_file)
351
+ old_pid = File.read(old_file).to_i
352
+ Process.kill(:QUIT, old_pid)
353
+ wait_for_death(old_pid)
354
+
355
+ ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
356
+ current_pid = File.read(pid_file).to_i
357
+ Process.kill(:USR2, current_pid)
358
+
359
+ # wait for pid_file to restore itself
360
+ tries = DEFAULT_TRIES
361
+ begin
362
+ while current_pid != File.read(pid_file).to_i
363
+ sleep(DEFAULT_RES) and (tries -= 1) > 0
364
+ end
365
+ rescue Errno::ENOENT
366
+ (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
367
+ end
368
+ assert_equal current_pid, File.read(pid_file).to_i
369
+
370
+ tries = DEFAULT_TRIES
371
+ while File.exist?(old_file)
372
+ (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
373
+ end
374
+ assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
375
+ port2 = unused_port(@addr)
376
+
377
+ # fix the bug
378
+ ucfg.sysseek(0)
379
+ ucfg.truncate(0)
380
+ ucfg.syswrite("listen %(#@addr:#@port)\n")
381
+ ucfg.syswrite("listen %(#@addr:#{port2})\n")
382
+ ucfg.syswrite("pid %(#{pid_file})\n")
383
+ Process.kill(:USR2, current_pid)
384
+
385
+ wait_for_file(old_file)
386
+ wait_for_file(pid_file)
387
+ new_pid = File.read(pid_file).to_i
388
+ assert_not_equal current_pid, new_pid
389
+ assert_equal current_pid, File.read(old_file).to_i
390
+ results = retry_hit(["http://#{@addr}:#{@port}/",
391
+ "http://#{@addr}:#{port2}/"])
392
+ assert_equal String, results[0].class
393
+ assert_equal String, results[1].class
394
+
395
+ Process.kill(:QUIT, current_pid)
396
+ Process.kill(:QUIT, new_pid)
397
+ end
398
+
399
+ def test_broken_reexec_ru
400
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
401
+ pid_file = "#{@tmpdir}/test.pid"
402
+ old_file = "#{pid_file}.oldbin"
403
+ ucfg = Tempfile.new('unicorn_test_config')
404
+ ucfg.syswrite("pid %(#{pid_file})\n")
405
+ ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
406
+ pid = xfork do
407
+ redirect_test_io do
408
+ exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
409
+ end
410
+ end
411
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
412
+ assert_equal String, results[0].class
413
+
414
+ wait_for_file(pid_file)
415
+ Process.waitpid(pid)
416
+ Process.kill(:USR2, File.read(pid_file).to_i)
417
+ wait_for_file(old_file)
418
+ wait_for_file(pid_file)
419
+ old_pid = File.read(old_file).to_i
420
+ Process.kill(:QUIT, old_pid)
421
+ wait_for_death(old_pid)
422
+
423
+ File.unlink("config.ru") # break reloading
424
+ current_pid = File.read(pid_file).to_i
425
+ Process.kill(:USR2, current_pid)
426
+
427
+ # wait for pid_file to restore itself
428
+ tries = DEFAULT_TRIES
429
+ begin
430
+ while current_pid != File.read(pid_file).to_i
431
+ sleep(DEFAULT_RES) and (tries -= 1) > 0
432
+ end
433
+ rescue Errno::ENOENT
434
+ (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
435
+ end
436
+
437
+ tries = DEFAULT_TRIES
438
+ while File.exist?(old_file)
439
+ (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
440
+ end
441
+ assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
442
+ assert_equal current_pid, File.read(pid_file).to_i
443
+
444
+ # fix the bug
445
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
446
+ Process.kill(:USR2, current_pid)
447
+ wait_for_file(old_file)
448
+ wait_for_file(pid_file)
449
+ new_pid = File.read(pid_file).to_i
450
+ assert_not_equal current_pid, new_pid
451
+ assert_equal current_pid, File.read(old_file).to_i
452
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
453
+ assert_equal String, results[0].class
454
+
455
+ Process.kill(:QUIT, current_pid)
456
+ Process.kill(:QUIT, new_pid)
457
+ end
458
+
459
+ def test_unicorn_config_listener_swap
460
+ port_cli = unused_port
461
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
462
+ ucfg = Tempfile.new('unicorn_test_config')
463
+ ucfg.syswrite("listen '#@addr:#@port'\n")
464
+ pid = xfork do
465
+ redirect_test_io do
466
+ exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
467
+ end
468
+ end
469
+ results = retry_hit(["http://#@addr:#{port_cli}/"])
470
+ assert_equal String, results[0].class
471
+ results = retry_hit(["http://#@addr:#@port/"])
472
+ assert_equal String, results[0].class
473
+
474
+ port2 = unused_port(@addr)
475
+ ucfg.sysseek(0)
476
+ ucfg.truncate(0)
477
+ ucfg.syswrite("listen '#@addr:#{port2}'\n")
478
+ Process.kill(:HUP, pid)
479
+
480
+ results = retry_hit(["http://#@addr:#{port2}/"])
481
+ assert_equal String, results[0].class
482
+ results = retry_hit(["http://#@addr:#{port_cli}/"])
483
+ assert_equal String, results[0].class
484
+ reuse = TCPServer.new(@addr, @port)
485
+ reuse.close
486
+ assert_shutdown(pid)
487
+ end
488
+
489
+ def test_unicorn_config_listen_with_options
490
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
491
+ ucfg = Tempfile.new('unicorn_test_config')
492
+ ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
493
+ ucfg.syswrite(" :rcvbuf => 4096,\n")
494
+ ucfg.syswrite(" :sndbuf => 4096\n")
495
+ pid = xfork do
496
+ redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
497
+ end
498
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
499
+ assert_equal String, results[0].class
500
+ assert_shutdown(pid)
501
+ end
502
+
503
+ def test_unicorn_config_per_worker_listen
504
+ port2 = unused_port
505
+ pid_spit = 'use Rack::ContentLength;' \
506
+ 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
507
+ File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
508
+ tmp = Tempfile.new('test.socket')
509
+ File.unlink(tmp.path)
510
+ ucfg = Tempfile.new('unicorn_test_config')
511
+ ucfg.syswrite("listen '#@addr:#@port'\n")
512
+ ucfg.syswrite("after_fork { |s,w|\n")
513
+ ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
514
+ ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
515
+ ucfg.syswrite("\n}\n")
516
+ pid = xfork do
517
+ redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
518
+ end
519
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
520
+ assert_equal String, results[0].class
521
+ worker_pid = results[0].to_i
522
+ assert_not_equal pid, worker_pid
523
+ s = UNIXSocket.new(tmp.path)
524
+ s.syswrite("GET / HTTP/1.0\r\n\r\n")
525
+ results = ''
526
+ loop { results << s.sysread(4096) } rescue nil
527
+ s.close
528
+ assert_equal worker_pid, results.split(/\r\n/).last.to_i
529
+ results = hit(["http://#@addr:#{port2}/"])
530
+ assert_equal String, results[0].class
531
+ assert_equal worker_pid, results[0].to_i
532
+ assert_shutdown(pid)
533
+ end
534
+
535
+ def test_unicorn_config_listen_augments_cli
536
+ port2 = unused_port(@addr)
537
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
538
+ ucfg = Tempfile.new('unicorn_test_config')
539
+ ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
540
+ pid = xfork do
541
+ redirect_test_io do
542
+ exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
543
+ end
544
+ end
545
+ uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
546
+ results = retry_hit(uris)
547
+ assert_equal results.size, uris.size
548
+ assert_equal String, results[0].class
549
+ assert_equal String, results[1].class
550
+ assert_shutdown(pid)
551
+ end
552
+
553
+ def test_weird_config_settings
554
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
555
+ ucfg = Tempfile.new('unicorn_test_config')
556
+ ucfg.syswrite(HEAVY_CFG)
557
+ pid = xfork do
558
+ redirect_test_io do
559
+ exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
560
+ end
561
+ end
562
+
563
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
564
+ assert_equal String, results[0].class
565
+ wait_master_ready(COMMON_TMP.path)
566
+ wait_workers_ready(COMMON_TMP.path, 4)
567
+ bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
568
+ assert_equal 4, bf.size
569
+ rotate = Tempfile.new('unicorn_rotate')
570
+
571
+ File.rename(COMMON_TMP.path, rotate.path)
572
+ Process.kill(:USR1, pid)
573
+
574
+ wait_for_file(COMMON_TMP.path)
575
+ assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
576
+ # USR1 should've been passed to all workers
577
+ tries = DEFAULT_TRIES
578
+ log = File.readlines(rotate.path)
579
+ while (tries -= 1) > 0 &&
580
+ log.grep(/reopening logs\.\.\./).size < 5
581
+ sleep DEFAULT_RES
582
+ log = File.readlines(rotate.path)
583
+ end
584
+ assert_equal 5, log.grep(/reopening logs\.\.\./).size
585
+ assert_equal 0, log.grep(/done reopening logs/).size
586
+
587
+ tries = DEFAULT_TRIES
588
+ log = File.readlines(COMMON_TMP.path)
589
+ while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
590
+ sleep DEFAULT_RES
591
+ log = File.readlines(COMMON_TMP.path)
592
+ end
593
+ assert_equal 5, log.grep(/done reopening logs/).size
594
+ assert_equal 0, log.grep(/reopening logs\.\.\./).size
595
+
596
+ Process.kill(:QUIT, pid)
597
+ pid, status = Process.waitpid2(pid)
598
+
599
+ assert status.success?, "exited successfully"
600
+ end
601
+
602
+ def test_read_embedded_cli_switches
603
+ File.open("config.ru", "wb") do |fp|
604
+ fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
605
+ fp.syswrite(HI)
606
+ end
607
+ pid = fork { redirect_test_io { exec($unicorn_bin) } }
608
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
609
+ assert_equal String, results[0].class
610
+ assert_shutdown(pid)
611
+ end
612
+
613
+ def test_config_ru_alt_path
614
+ config_path = "#{@tmpdir}/foo.ru"
615
+ File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
616
+ pid = fork do
617
+ redirect_test_io do
618
+ Dir.chdir("/")
619
+ exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
620
+ end
621
+ end
622
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
623
+ assert_equal String, results[0].class
624
+ assert_shutdown(pid)
625
+ end
626
+
627
+ def test_load_module
628
+ libdir = "#{@tmpdir}/lib"
629
+ FileUtils.mkpath([ libdir ])
630
+ config_path = "#{libdir}/hello.rb"
631
+ File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
632
+ pid = fork do
633
+ redirect_test_io do
634
+ Dir.chdir("/")
635
+ exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
636
+ end
637
+ end
638
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
639
+ assert_equal String, results[0].class
640
+ assert_shutdown(pid)
641
+ end
642
+
643
+ def test_reexec
644
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
645
+ pid_file = "#{@tmpdir}/test.pid"
646
+ pid = fork do
647
+ redirect_test_io do
648
+ exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
649
+ end
650
+ end
651
+ reexec_basic_test(pid, pid_file)
652
+ end
653
+
654
+ def test_reexec_alt_config
655
+ config_file = "#{@tmpdir}/foo.ru"
656
+ File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
657
+ pid_file = "#{@tmpdir}/test.pid"
658
+ pid = fork do
659
+ redirect_test_io do
660
+ exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
661
+ end
662
+ end
663
+ reexec_basic_test(pid, pid_file)
664
+ end
665
+
666
+ def test_socket_unlinked_restore
667
+ results = nil
668
+ sock = Tempfile.new('unicorn_test_sock')
669
+ sock_path = sock.path
670
+ @sockets << sock_path
671
+ sock.close!
672
+ ucfg = Tempfile.new('unicorn_test_config')
673
+ ucfg.syswrite("listen \"#{sock_path}\"\n")
674
+
675
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
676
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
677
+ wait_for_file(sock_path)
678
+ assert File.socket?(sock_path)
679
+
680
+ sock = UNIXSocket.new(sock_path)
681
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
682
+ results = sock.sysread(4096)
683
+
684
+ assert_equal String, results.class
685
+ File.unlink(sock_path)
686
+ Process.kill(:HUP, pid)
687
+ wait_for_file(sock_path)
688
+ assert File.socket?(sock_path)
689
+
690
+ sock = UNIXSocket.new(sock_path)
691
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
692
+ results = sock.sysread(4096)
693
+
694
+ assert_equal String, results.class
695
+ end
696
+
697
+ def test_unicorn_config_file
698
+ pid_file = "#{@tmpdir}/test.pid"
699
+ sock = Tempfile.new('unicorn_test_sock')
700
+ sock_path = sock.path
701
+ sock.close!
702
+ @sockets << sock_path
703
+
704
+ log = Tempfile.new('unicorn_test_log')
705
+ ucfg = Tempfile.new('unicorn_test_config')
706
+ ucfg.syswrite("listen \"#{sock_path}\"\n")
707
+ ucfg.syswrite("pid \"#{pid_file}\"\n")
708
+ ucfg.syswrite("logger Logger.new('#{log.path}')\n")
709
+ ucfg.close
710
+
711
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
712
+ pid = xfork do
713
+ redirect_test_io do
714
+ exec($unicorn_bin, "-l#{@addr}:#{@port}",
715
+ "-P#{pid_file}", "-c#{ucfg.path}")
716
+ end
717
+ end
718
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
719
+ assert_equal String, results[0].class
720
+ wait_master_ready(log.path)
721
+ assert File.exist?(pid_file), "pid_file created"
722
+ assert_equal pid, File.read(pid_file).to_i
723
+ assert File.socket?(sock_path), "socket created"
724
+
725
+ sock = UNIXSocket.new(sock_path)
726
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
727
+ results = sock.sysread(4096)
728
+
729
+ assert_equal String, results.class
730
+
731
+ # try reloading the config
732
+ sock = Tempfile.new('new_test_sock')
733
+ new_sock_path = sock.path
734
+ @sockets << new_sock_path
735
+ sock.close!
736
+ new_log = Tempfile.new('unicorn_test_log')
737
+ new_log.sync = true
738
+ assert_equal 0, new_log.size
739
+
740
+ ucfg = File.open(ucfg.path, "wb")
741
+ ucfg.syswrite("listen \"#{sock_path}\"\n")
742
+ ucfg.syswrite("listen \"#{new_sock_path}\"\n")
743
+ ucfg.syswrite("pid \"#{pid_file}\"\n")
744
+ ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
745
+ ucfg.close
746
+ Process.kill(:HUP, pid)
747
+
748
+ wait_for_file(new_sock_path)
749
+ assert File.socket?(new_sock_path), "socket exists"
750
+ @sockets.each do |path|
751
+ sock = UNIXSocket.new(path)
752
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
753
+ results = sock.sysread(4096)
754
+ assert_equal String, results.class
755
+ end
756
+
757
+ assert_not_equal 0, new_log.size
758
+ reexec_usr2_quit_test(pid, pid_file)
759
+ end
760
+
761
+ def test_daemonize_reexec
762
+ pid_file = "#{@tmpdir}/test.pid"
763
+ log = Tempfile.new('unicorn_test_log')
764
+ ucfg = Tempfile.new('unicorn_test_config')
765
+ ucfg.syswrite("pid \"#{pid_file}\"\n")
766
+ ucfg.syswrite("logger Logger.new('#{log.path}')\n")
767
+ ucfg.close
768
+
769
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
770
+ pid = xfork do
771
+ redirect_test_io do
772
+ exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
773
+ end
774
+ end
775
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
776
+ assert_equal String, results[0].class
777
+ wait_for_file(pid_file)
778
+ new_pid = File.read(pid_file).to_i
779
+ assert_not_equal pid, new_pid
780
+ pid, status = Process.waitpid2(pid)
781
+ assert status.success?, "original process exited successfully"
782
+ Process.kill(0, new_pid)
783
+ reexec_usr2_quit_test(new_pid, pid_file)
784
+ end
785
+
786
+ def test_daemonize_redirect_fail
787
+ pid_file = "#{@tmpdir}/test.pid"
788
+ ucfg = Tempfile.new('unicorn_test_config')
789
+ ucfg.syswrite("pid #{pid_file}\"\n")
790
+ err = Tempfile.new('stderr')
791
+ out = Tempfile.new('stdout ')
792
+
793
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
794
+ pid = xfork do
795
+ $stderr.reopen(err.path, "a")
796
+ $stdout.reopen(out.path, "a")
797
+ exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
798
+ end
799
+ pid, status = Process.waitpid2(pid)
800
+ assert ! status.success?, "original process exited successfully"
801
+ sleep 1 # can't waitpid on a daemonized process :<
802
+ assert err.stat.size > 0
803
+ end
804
+
805
+ def test_reexec_fd_leak
806
+ unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
807
+ warn "FD leak test only works on Linux at the moment"
808
+ return
809
+ end
810
+ pid_file = "#{@tmpdir}/test.pid"
811
+ log = Tempfile.new('unicorn_test_log')
812
+ log.sync = true
813
+ ucfg = Tempfile.new('unicorn_test_config')
814
+ ucfg.syswrite("pid \"#{pid_file}\"\n")
815
+ ucfg.syswrite("logger Logger.new('#{log.path}')\n")
816
+ ucfg.syswrite("stderr_path '#{log.path}'\n")
817
+ ucfg.syswrite("stdout_path '#{log.path}'\n")
818
+ ucfg.close
819
+
820
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
821
+ pid = xfork do
822
+ redirect_test_io do
823
+ exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
824
+ end
825
+ end
826
+
827
+ wait_master_ready(log.path)
828
+ wait_workers_ready(log.path, 1)
829
+ File.truncate(log.path, 0)
830
+ wait_for_file(pid_file)
831
+ orig_pid = pid = File.read(pid_file).to_i
832
+ orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
833
+ assert $?.success?
834
+ expect_size = orig_fds.size
835
+
836
+ Process.kill(:USR2, pid)
837
+ wait_for_file("#{pid_file}.oldbin")
838
+ Process.kill(:QUIT, pid)
839
+
840
+ wait_for_death(pid)
841
+
842
+ wait_master_ready(log.path)
843
+ wait_workers_ready(log.path, 1)
844
+ File.truncate(log.path, 0)
845
+ wait_for_file(pid_file)
846
+ pid = File.read(pid_file).to_i
847
+ assert_not_equal orig_pid, pid
848
+ curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
849
+ assert $?.success?
850
+
851
+ # we could've inherited descriptors the first time around
852
+ assert expect_size >= curr_fds.size, curr_fds.inspect
853
+ expect_size = curr_fds.size
854
+
855
+ Process.kill(:USR2, pid)
856
+ wait_for_file("#{pid_file}.oldbin")
857
+ Process.kill(:QUIT, pid)
858
+
859
+ wait_for_death(pid)
860
+
861
+ wait_master_ready(log.path)
862
+ wait_workers_ready(log.path, 1)
863
+ File.truncate(log.path, 0)
864
+ wait_for_file(pid_file)
865
+ pid = File.read(pid_file).to_i
866
+ curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
867
+ assert $?.success?
868
+ assert_equal expect_size, curr_fds.size, curr_fds.inspect
869
+
870
+ Process.kill(:QUIT, pid)
871
+ wait_for_death(pid)
872
+ end
873
+
874
+ def hup_test_common(preload)
875
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
876
+ pid_file = Tempfile.new('pid')
877
+ ucfg = Tempfile.new('unicorn_test_config')
878
+ ucfg.syswrite("listen '#@addr:#@port'\n")
879
+ ucfg.syswrite("pid '#{pid_file.path}'\n")
880
+ ucfg.syswrite("preload_app true\n") if preload
881
+ ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
882
+ ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
883
+ pid = xfork {
884
+ redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
885
+ }
886
+ _, status = Process.waitpid2(pid)
887
+ assert status.success?
888
+ wait_master_ready("test_stderr.#$$.log")
889
+ wait_workers_ready("test_stderr.#$$.log", 1)
890
+ uri = URI.parse("http://#@addr:#@port/")
891
+ pids = Tempfile.new('worker_pids')
892
+ r, w = IO.pipe
893
+ hitter = fork {
894
+ r.close
895
+ bodies = Hash.new(0)
896
+ at_exit { pids.syswrite(bodies.inspect) }
897
+ trap(:TERM) { exit(0) }
898
+ nr = 0
899
+ loop {
900
+ rv = Net::HTTP.get(uri)
901
+ pid = rv.to_i
902
+ exit!(1) if pid <= 0
903
+ bodies[pid] += 1
904
+ nr += 1
905
+ if nr == 1
906
+ w.syswrite('1')
907
+ elsif bodies.size > 1
908
+ w.syswrite('2')
909
+ sleep
910
+ end
911
+ }
912
+ }
913
+ w.close
914
+ assert_equal '1', r.read(1)
915
+ daemon_pid = File.read(pid_file.path).to_i
916
+ assert daemon_pid > 0
917
+ Process.kill(:HUP, daemon_pid)
918
+ assert_equal '2', r.read(1)
919
+ Process.kill(:TERM, hitter)
920
+ _, hitter_status = Process.waitpid2(hitter)
921
+ assert(hitter_status.success?,
922
+ "invalid: #{hitter_status.inspect} #{File.read(pids.path)}" \
923
+ "#{File.read("test_stderr.#$$.log")}")
924
+ pids.sysseek(0)
925
+ pids = eval(pids.read)
926
+ assert_kind_of(Hash, pids)
927
+ assert_equal 2, pids.size
928
+ pids.keys.each { |x|
929
+ assert_kind_of(Integer, x)
930
+ assert x > 0
931
+ assert pids[x] > 0
932
+ }
933
+ Process.kill(:QUIT, daemon_pid)
934
+ wait_for_death(daemon_pid)
935
+ end
936
+
937
+ def test_preload_app_hup
938
+ hup_test_common(true)
939
+ end
940
+
941
+ def test_hup
942
+ hup_test_common(false)
943
+ end
944
+
945
+ def test_default_listen_hup_holds_listener
946
+ default_listen_lock do
947
+ res, pid_path = default_listen_setup
948
+ daemon_pid = File.read(pid_path).to_i
949
+ Process.kill(:HUP, daemon_pid)
950
+ wait_workers_ready("test_stderr.#$$.log", 1)
951
+ res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
952
+ assert_match %r{\d+}, res2.first
953
+ assert res2.first != res.first
954
+ Process.kill(:QUIT, daemon_pid)
955
+ wait_for_death(daemon_pid)
956
+ end
957
+ end
958
+
959
+ def test_default_listen_upgrade_holds_listener
960
+ default_listen_lock do
961
+ res, pid_path = default_listen_setup
962
+ daemon_pid = File.read(pid_path).to_i
963
+
964
+ Process.kill(:USR2, daemon_pid)
965
+ wait_for_file("#{pid_path}.oldbin")
966
+ wait_for_file(pid_path)
967
+ Process.kill(:QUIT, daemon_pid)
968
+ wait_for_death(daemon_pid)
969
+
970
+ daemon_pid = File.read(pid_path).to_i
971
+ wait_workers_ready("test_stderr.#$$.log", 1)
972
+ File.truncate("test_stderr.#$$.log", 0)
973
+
974
+ res2 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
975
+ assert_match %r{\d+}, res2.first
976
+ assert res2.first != res.first
977
+
978
+ Process.kill(:HUP, daemon_pid)
979
+ wait_workers_ready("test_stderr.#$$.log", 1)
980
+ File.truncate("test_stderr.#$$.log", 0)
981
+ res3 = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
982
+ assert res2.first != res3.first
983
+
984
+ Process.kill(:QUIT, daemon_pid)
985
+ wait_for_death(daemon_pid)
986
+ end
987
+ end
988
+
989
+ def default_listen_setup
990
+ File.open("config.ru", "wb") { |fp| fp.syswrite(HI.gsub("HI", '#$$')) }
991
+ pid_path = (tmp = Tempfile.new('pid')).path
992
+ tmp.close!
993
+ ucfg = Tempfile.new('unicorn_test_config')
994
+ ucfg.syswrite("pid '#{pid_path}'\n")
995
+ ucfg.syswrite("stderr_path 'test_stderr.#$$.log'\n")
996
+ ucfg.syswrite("stdout_path 'test_stdout.#$$.log'\n")
997
+ pid = xfork {
998
+ redirect_test_io { exec($unicorn_bin, "-D", "-c", ucfg.path) }
999
+ }
1000
+ _, status = Process.waitpid2(pid)
1001
+ assert status.success?
1002
+ wait_master_ready("test_stderr.#$$.log")
1003
+ wait_workers_ready("test_stderr.#$$.log", 1)
1004
+ File.truncate("test_stderr.#$$.log", 0)
1005
+ res = hit(["http://#{Unicorn::Const::DEFAULT_LISTEN}/"])
1006
+ assert_match %r{\d+}, res.first
1007
+ [ res, pid_path ]
1008
+ end
1009
+
1010
+ # we need to flock() something to prevent these tests from running
1011
+ def default_listen_lock(&block)
1012
+ fp = File.open(FLOCK_PATH, "rb")
1013
+ begin
1014
+ fp.flock(File::LOCK_EX)
1015
+ begin
1016
+ TCPServer.new(Unicorn::Const::DEFAULT_HOST,
1017
+ Unicorn::Const::DEFAULT_PORT).close
1018
+ rescue Errno::EADDRINUSE, Errno::EACCES
1019
+ warn "can't bind to #{Unicorn::Const::DEFAULT_LISTEN}"
1020
+ return false
1021
+ end
1022
+
1023
+ # unused_port should never take this, but we may run an environment
1024
+ # where tests are being run against older unicorns...
1025
+ lock_path = "#{Dir::tmpdir}/unicorn_test." \
1026
+ "#{Unicorn::Const::DEFAULT_LISTEN}.lock"
1027
+ begin
1028
+ File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
1029
+ yield
1030
+ rescue Errno::EEXIST
1031
+ lock_path = nil
1032
+ return false
1033
+ ensure
1034
+ File.unlink(lock_path) if lock_path
1035
+ end
1036
+ ensure
1037
+ fp.flock(File::LOCK_UN)
1038
+ end
1039
+ end
1040
+
1041
+ end if do_test