boourns-unicorn 4.4.1

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.
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