style: symbols instead of strings for signal names
[unicorn.git] / test / exec / test_exec.rb
blobcaada3b0491054a918d94f30bc549ec035f69c8a
1 # Copyright (c) 2009 Eric Wong
2 STDIN.sync = STDOUT.sync = STDERR.sync = true
3 require 'test/test_helper'
4 require 'pathname'
5 require 'tempfile'
6 require 'fileutils'
8 do_test = true
9 DEFAULT_TRIES = 1000
10 DEFAULT_RES = 0.2
12 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
13 redirect_test_io do
14   do_test = system($unicorn_bin, '-v')
15 end
17 unless do_test
18   STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
19               "skipping this test"
20 end
22 begin
23   require 'rack'
24 rescue LoadError
25   STDERR.puts "Unable to load Rack, skipping this test"
26   do_test = false
27 end
29 class ExecTest < Test::Unit::TestCase
30   trap(:QUIT, 'IGNORE')
32   HI = <<-EOS
33 use Rack::ContentLength
34 run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] }
35   EOS
37   HELLO = <<-EOS
38 class Hello
39   def call(env)
40     [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ]
41   end
42 end
43   EOS
45   COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
47   HEAVY_CFG = <<-EOS
48 worker_processes 4
49 timeout 30
50 backlog 128
51 logger Logger.new('#{COMMON_TMP.path}')
52 before_fork do |server, worker_nr|
53   server.logger.info "before_fork: worker=\#{worker_nr}"
54 end
55   EOS
57   def setup
58     @pwd = Dir.pwd
59     @tmpfile = Tempfile.new('unicorn_exec_test')
60     @tmpdir = @tmpfile.path
61     @tmpfile.close!
62     Dir.mkdir(@tmpdir)
63     Dir.chdir(@tmpdir)
64     @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
65     @port = unused_port(@addr)
66     @sockets = []
67     @start_pid = $$
68   end
70   def teardown
71     return if @start_pid != $$
72     Dir.chdir(@pwd)
73     FileUtils.rmtree(@tmpdir)
74     @sockets.each { |path| File.unlink(path) rescue nil }
75     loop do
76       Process.kill('-QUIT', 0)
77       begin
78         Process.waitpid(-1, Process::WNOHANG) or break
79       rescue Errno::ECHILD
80         break
81       end
82     end
83   end
85   def test_basic
86     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
87     pid = fork do
88       redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
89     end
90     results = retry_hit(["http://#{@addr}:#{@port}/"])
91     assert_equal String, results[0].class
92     assert_shutdown(pid)
93   end
95   def test_help
96     redirect_test_io do
97       assert(system($unicorn_bin, "-h"), "help text returns true")
98     end
99     assert_equal 0, File.stat("test_stderr.#$$.log").size
100     assert_not_equal 0, File.stat("test_stdout.#$$.log").size
101     lines = File.readlines("test_stdout.#$$.log")
103     # Be considerate of the on-call technician working from their
104     # mobile phone or netbook on a slow connection :)
105     assert lines.size <= 24, "help height fits in an ANSI terminal window"
106     lines.each do |line|
107       assert line.size <= 80, "help width fits in an ANSI terminal window"
108     end
109   end
111   def test_broken_reexec_config
112     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
113     pid_file = "#{@tmpdir}/test.pid"
114     old_file = "#{pid_file}.oldbin"
115     ucfg = Tempfile.new('unicorn_test_config')
116     ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
117     ucfg.syswrite("pid %(#{pid_file})\n")
118     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
119     pid = xfork do
120       redirect_test_io do
121         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
122       end
123     end
124     results = retry_hit(["http://#{@addr}:#{@port}/"])
125     assert_equal String, results[0].class
127     wait_for_file(pid_file)
128     Process.waitpid(pid)
129     Process.kill(:USR2, File.read(pid_file).to_i)
130     wait_for_file(old_file)
131     wait_for_file(pid_file)
132     Process.kill(:QUIT, File.read(old_file).to_i)
134     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
135     current_pid = File.read(pid_file).to_i
136     Process.kill(:USR2, current_pid)
138     # wait for pid_file to restore itself
139     tries = DEFAULT_TRIES
140     begin
141       while current_pid != File.read(pid_file).to_i
142         sleep(DEFAULT_RES) and (tries -= 1) > 0
143       end
144     rescue Errno::ENOENT
145       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
146     end
147     assert_equal current_pid, File.read(pid_file).to_i
149     tries = DEFAULT_TRIES
150     while File.exist?(old_file)
151       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
152     end
153     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
154     port2 = unused_port(@addr)
156     # fix the bug
157     ucfg.sysseek(0)
158     ucfg.truncate(0)
159     ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n")
160     ucfg.syswrite("pid %(#{pid_file})\n")
161     Process.kill(:USR2, current_pid)
162     wait_for_file(old_file)
163     wait_for_file(pid_file)
164     new_pid = File.read(pid_file).to_i
165     assert_not_equal current_pid, new_pid
166     assert_equal current_pid, File.read(old_file).to_i
167     results = retry_hit(["http://#{@addr}:#{@port}/",
168                          "http://#{@addr}:#{port2}/"])
169     assert_equal String, results[0].class
170     assert_equal String, results[1].class
172     assert_nothing_raised do
173       Process.kill(:QUIT, current_pid)
174       Process.kill(:QUIT, new_pid)
175     end
176   end
178   def test_broken_reexec_ru
179     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
180     pid_file = "#{@tmpdir}/test.pid"
181     old_file = "#{pid_file}.oldbin"
182     ucfg = Tempfile.new('unicorn_test_config')
183     ucfg.syswrite("pid %(#{pid_file})\n")
184     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
185     pid = xfork do
186       redirect_test_io do
187         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
188       end
189     end
190     results = retry_hit(["http://#{@addr}:#{@port}/"])
191     assert_equal String, results[0].class
193     wait_for_file(pid_file)
194     Process.waitpid(pid)
195     Process.kill(:USR2, File.read(pid_file).to_i)
196     wait_for_file(old_file)
197     wait_for_file(pid_file)
198     Process.kill(:QUIT, File.read(old_file).to_i)
200     File.unlink("config.ru") # break reloading
201     current_pid = File.read(pid_file).to_i
202     Process.kill(:USR2, current_pid)
204     # wait for pid_file to restore itself
205     tries = DEFAULT_TRIES
206     begin
207       while current_pid != File.read(pid_file).to_i
208         sleep(DEFAULT_RES) and (tries -= 1) > 0
209       end
210     rescue Errno::ENOENT
211       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
212     end
213     assert_equal current_pid, File.read(pid_file).to_i
215     tries = DEFAULT_TRIES
216     while File.exist?(old_file)
217       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
218     end
219     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
221     # fix the bug
222     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
223     Process.kill(:USR2, current_pid)
224     wait_for_file(old_file)
225     wait_for_file(pid_file)
226     new_pid = File.read(pid_file).to_i
227     assert_not_equal current_pid, new_pid
228     assert_equal current_pid, File.read(old_file).to_i
229     results = retry_hit(["http://#{@addr}:#{@port}/"])
230     assert_equal String, results[0].class
232     assert_nothing_raised do
233       Process.kill(:QUIT, current_pid)
234       Process.kill(:QUIT, new_pid)
235     end
236   end
238   def test_unicorn_config_listeners_overrides_cli
239     port2 = unused_port(@addr)
240     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
241     # listeners = [ ... ]  => should _override_ command-line options
242     ucfg = Tempfile.new('unicorn_test_config')
243     ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
244     pid = xfork do
245       redirect_test_io do
246         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
247       end
248     end
249     results = retry_hit(["http://#{@addr}:#{@port}/"])
250     assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
251     assert_equal String, results[0].class
252     assert_shutdown(pid)
253   end
255   def test_unicorn_config_listen_augments_cli
256     port2 = unused_port(@addr)
257     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
258     ucfg = Tempfile.new('unicorn_test_config')
259     ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
260     pid = xfork do
261       redirect_test_io do
262         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
263       end
264     end
265     uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
266     results = retry_hit(uris)
267     assert_equal results.size, uris.size
268     assert_equal String, results[0].class
269     assert_equal String, results[1].class
270     assert_shutdown(pid)
271   end
273   def test_weird_config_settings
274     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
275     ucfg = Tempfile.new('unicorn_test_config')
276     ucfg.syswrite(HEAVY_CFG)
277     pid = xfork do
278       redirect_test_io do
279         exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
280       end
281     end
283     results = retry_hit(["http://#{@addr}:#{@port}/"])
284     assert_equal String, results[0].class
285     wait_master_ready(COMMON_TMP.path)
286     wait_workers_ready(COMMON_TMP.path, 4)
287     bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
288     assert_equal 4, bf.size
289     rotate = Tempfile.new('unicorn_rotate')
290     assert_nothing_raised do
291       File.rename(COMMON_TMP.path, rotate.path)
292       Process.kill(:USR1, pid)
293     end
294     wait_for_file(COMMON_TMP.path)
295     assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
296     # USR1 should've been passed to all workers
297     tries = DEFAULT_TRIES
298     log = File.readlines(rotate.path)
299     while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4
300       sleep DEFAULT_RES
301       log = File.readlines(rotate.path)
302     end
303     assert_equal 4, log.grep(/worker=\d+ rotating logs\.\.\./).size
304     assert_equal 0, log.grep(/done rotating logs/).size
306     tries = DEFAULT_TRIES
307     log = File.readlines(COMMON_TMP.path)
308     while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4
309       sleep DEFAULT_RES
310       log = File.readlines(COMMON_TMP.path)
311     end
312     assert_equal 4, log.grep(/worker=\d+ done rotating logs/).size
313     assert_equal 0, log.grep(/rotating logs\.\.\./).size
314     assert_nothing_raised { Process.kill(:QUIT, pid) }
315     status = nil
316     assert_nothing_raised { pid, status = Process.waitpid2(pid) }
317     assert status.success?, "exited successfully"
318   end
320   def test_read_embedded_cli_switches
321     File.open("config.ru", "wb") do |fp|
322       fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
323       fp.syswrite(HI)
324     end
325     pid = fork { redirect_test_io { exec($unicorn_bin) } }
326     results = retry_hit(["http://#{@addr}:#{@port}/"])
327     assert_equal String, results[0].class
328     assert_shutdown(pid)
329   end
331   def test_config_ru_alt_path
332     config_path = "#{@tmpdir}/foo.ru"
333     File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
334     pid = fork do
335       redirect_test_io do
336         Dir.chdir("/")
337         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
338       end
339     end
340     results = retry_hit(["http://#{@addr}:#{@port}/"])
341     assert_equal String, results[0].class
342     assert_shutdown(pid)
343   end
345   def test_load_module
346     libdir = "#{@tmpdir}/lib"
347     FileUtils.mkpath([ libdir ])
348     config_path = "#{libdir}/hello.rb"
349     File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
350     pid = fork do
351       redirect_test_io do
352         Dir.chdir("/")
353         exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
354       end
355     end
356     results = retry_hit(["http://#{@addr}:#{@port}/"])
357     assert_equal String, results[0].class
358     assert_shutdown(pid)
359   end
361   def test_reexec
362     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
363     pid_file = "#{@tmpdir}/test.pid"
364     pid = fork do
365       redirect_test_io do
366         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
367       end
368     end
369     reexec_basic_test(pid, pid_file)
370   end
372   def test_reexec_alt_config
373     config_file = "#{@tmpdir}/foo.ru"
374     File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
375     pid_file = "#{@tmpdir}/test.pid"
376     pid = fork do
377       redirect_test_io do
378         exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
379       end
380     end
381     reexec_basic_test(pid, pid_file)
382   end
384   def test_unicorn_config_file
385     pid_file = "#{@tmpdir}/test.pid"
386     sock = Tempfile.new('unicorn_test_sock')
387     sock_path = sock.path
388     sock.close!
389     @sockets << sock_path
391     log = Tempfile.new('unicorn_test_log')
392     ucfg = Tempfile.new('unicorn_test_config')
393     ucfg.syswrite("listen \"#{sock_path}\"\n")
394     ucfg.syswrite("pid \"#{pid_file}\"\n")
395     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
396     ucfg.close
398     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
399     pid = xfork do
400       redirect_test_io do
401         exec($unicorn_bin, "-l#{@addr}:#{@port}",
402              "-P#{pid_file}", "-c#{ucfg.path}")
403       end
404     end
405     results = retry_hit(["http://#{@addr}:#{@port}/"])
406     assert_equal String, results[0].class
407     wait_master_ready(log.path)
408     assert File.exist?(pid_file), "pid_file created"
409     assert_equal pid, File.read(pid_file).to_i
410     assert File.socket?(sock_path), "socket created"
411     assert_nothing_raised do
412       sock = UNIXSocket.new(sock_path)
413       sock.syswrite("GET / HTTP/1.0\r\n\r\n")
414       results = sock.sysread(4096)
415     end
416     assert_equal String, results.class
418     # try reloading the config
419     sock = Tempfile.new('unicorn_test_sock')
420     new_sock_path = sock.path
421     @sockets << new_sock_path
422     sock.close!
423     new_log = Tempfile.new('unicorn_test_log')
424     new_log.sync = true
425     assert_equal 0, new_log.size
427     assert_nothing_raised do
428       ucfg = File.open(ucfg.path, "wb")
429       ucfg.syswrite("listen \"#{new_sock_path}\"\n")
430       ucfg.syswrite("pid \"#{pid_file}\"\n")
431       ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
432       ucfg.close
433       Process.kill(:HUP, pid)
434     end
436     wait_for_file(new_sock_path)
437     assert File.socket?(new_sock_path), "socket exists"
438     @sockets.each do |path|
439       assert_nothing_raised do
440         sock = UNIXSocket.new(path)
441         sock.syswrite("GET / HTTP/1.0\r\n\r\n")
442         results = sock.sysread(4096)
443       end
444       assert_equal String, results.class
445     end
447     assert_not_equal 0, new_log.size
448     reexec_usr2_quit_test(pid, pid_file)
449   end
451   def test_daemonize_reexec
452     pid_file = "#{@tmpdir}/test.pid"
453     log = Tempfile.new('unicorn_test_log')
454     ucfg = Tempfile.new('unicorn_test_config')
455     ucfg.syswrite("pid \"#{pid_file}\"\n")
456     ucfg.syswrite("logger Logger.new('#{log.path}')\n")
457     ucfg.close
459     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
460     pid = xfork do
461       redirect_test_io do
462         exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
463       end
464     end
465     results = retry_hit(["http://#{@addr}:#{@port}/"])
466     assert_equal String, results[0].class
467     wait_for_file(pid_file)
468     new_pid = File.read(pid_file).to_i
469     assert_not_equal pid, new_pid
470     pid, status = Process.waitpid2(pid)
471     assert status.success?, "original process exited successfully"
472     assert_nothing_raised { Process.kill(0, new_pid) }
473     reexec_usr2_quit_test(new_pid, pid_file)
474   end
476   private
478     # sometimes the server may not come up right away
479     def retry_hit(uris = [])
480       tries = DEFAULT_TRIES
481       begin
482         hit(uris)
483       rescue Errno::ECONNREFUSED => err
484         if (tries -= 1) > 0
485           sleep DEFAULT_RES
486           retry
487         end
488         raise err
489       end
490     end
492     def assert_shutdown(pid)
493       wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log")
494       assert_nothing_raised { Process.kill(:QUIT, pid) }
495       status = nil
496       assert_nothing_raised { pid, status = Process.waitpid2(pid) }
497       assert status.success?, "exited successfully"
498     end
500     def wait_workers_ready(path, nr_workers)
501       tries = DEFAULT_TRIES
502       lines = []
503       while (tries -= 1) > 0
504         begin
505           lines = File.readlines(path).grep(/worker=\d+ ready/)
506           lines.size == nr_workers and return
507         rescue Errno::ENOENT
508         end
509         sleep DEFAULT_RES
510       end
511       raise "#{nr_workers} workers never became ready:" \
512             "\n\t#{lines.join("\n\t")}\n"
513     end
515     def wait_master_ready(master_log)
516       tries = DEFAULT_TRIES
517       while (tries -= 1) > 0
518         begin
519           File.readlines(master_log).grep(/master process ready/)[0] and return
520         rescue Errno::ENOENT
521         end
522         sleep DEFAULT_RES
523       end
524       raise "master process never became ready"
525     end
527     def reexec_usr2_quit_test(pid, pid_file)
528       assert File.exist?(pid_file), "pid file OK"
529       assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
530       assert_nothing_raised { Process.kill(:USR2, pid) }
531       assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
532       wait_for_file("#{pid_file}.oldbin")
533       wait_for_file(pid_file)
535       # kill old master process
536       assert_not_equal pid, File.read(pid_file).to_i
537       assert_equal pid, File.read("#{pid_file}.oldbin").to_i
538       assert_nothing_raised { Process.kill(:QUIT, pid) }
539       assert_not_equal pid, File.read(pid_file).to_i
540       assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
541       wait_for_file(pid_file)
542       assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
543       assert_nothing_raised { Process.kill(:QUIT, File.read(pid_file).to_i) }
544     end
546     def reexec_basic_test(pid, pid_file)
547       results = retry_hit(["http://#{@addr}:#{@port}/"])
548       assert_equal String, results[0].class
549       assert_nothing_raised { Process.kill(0, pid) }
550       master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
551       wait_master_ready(master_log)
552       File.truncate(master_log, 0)
553       nr = 50
554       kill_point = 2
555       assert_nothing_raised do
556         nr.times do |i|
557           hit(["http://#{@addr}:#{@port}/#{i}"])
558           i == kill_point and Process.kill(:HUP, pid)
559         end
560       end
561       wait_master_ready(master_log)
562       assert File.exist?(pid_file), "pid=#{pid_file} exists"
563       new_pid = File.read(pid_file).to_i
564       assert_not_equal pid, new_pid
565       assert_nothing_raised { Process.kill(0, new_pid) }
566       assert_nothing_raised { Process.kill(:QUIT, new_pid) }
567     end
569     def wait_for_file(path)
570       tries = DEFAULT_TRIES
571       while (tries -= 1) > 0 && ! File.exist?(path)
572         sleep DEFAULT_RES
573       end
574       assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
575     end
577     def xfork(&block)
578       fork do
579         ObjectSpace.each_object(Tempfile) do |tmp|
580           ObjectSpace.undefine_finalizer(tmp)
581         end
582         yield
583       end
584     end
586 end if do_test