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