diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..b7a1a046be0b785d7ec7b9b4cb0b1897ed381acd --- /dev/null +++ b/app/controllers/unicorn_test_controller.rb @@ -0,0 +1,12 @@ +if Rails.env.test? + class UnicornTestController < ActionController::Base + def pid + render plain: Process.pid.to_s + end + + def kill + Process.kill(params[:signal], Process.pid) + render plain: 'Bye!' + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 1da226a3b575b54aefbe8343b4c60261e55dabe8..2584981bb040e3448f462fe7516e9fd9b7d40301 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -99,5 +99,7 @@ Rails.application.routes.draw do end end + draw :test if Rails.env.test? + get '*unmatched_route', to: 'application#route_not_found' end diff --git a/config/routes/test.rb b/config/routes/test.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac477cdbbbc2c69a892df513b05ed6665686569a --- /dev/null +++ b/config/routes/test.rb @@ -0,0 +1,2 @@ +get '/unicorn_test/pid' => 'unicorn_test#pid' +post '/unicorn_test/kill' => 'unicorn_test#kill' diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake index 2301ec9b2288c16b0a32701e40c679a8f107dfcd..99b3168d9eb85c7c43073918f3222c9a410dda1c 100644 --- a/lib/tasks/brakeman.rake +++ b/lib/tasks/brakeman.rake @@ -2,7 +2,7 @@ desc 'Security check via brakeman' task :brakeman do # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge # requests are welcome! - if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z)) + if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb,app/controllers/unicorn_test_controller.rb -w3 -z)) puts 'Security check succeed' else puts 'Security check failed' diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8518c047a470e573aafad5f185d3eaa931bd444e --- /dev/null +++ b/spec/unicorn/unicorn_spec.rb @@ -0,0 +1,98 @@ +require 'fileutils' + +require 'excon' + +require 'spec_helper' + +describe 'Unicorn' do + before(:all) do + config_lines = File.read('config/unicorn.rb.example').split("\n") + + # Remove these because they make setup harder. + config_lines = config_lines.reject do |line| + %w[ + working_directory + worker_processes + listen + pid + stderr_path + stdout_path + ].any? { |prefix| line.start_with?(prefix) } + end + + config_lines << "working_directory '#{Rails.root}'" + + # We want to have exactly 1 worker process because that makes it + # predictable which process will handle our requests. + config_lines << 'worker_processes 1' + + @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket') + config_lines << "listen '#{@socket_path}'" + + ready_file = 'tmp/tests/unicorn-worker-ready' + FileUtils.rm_f(ready_file) + after_fork_index = config_lines.index { |l| l.start_with?('after_fork') } + config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)") + + config_path = 'tmp/tests/unicorn.rb' + File.write(config_path, config_lines.join("\n") + "\n") + + cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}] + @unicorn_master_pid = spawn(*cmd) + wait_unicorn_boot!(@unicorn_master_pid, ready_file) + WebMock.allow_net_connect! + end + + %w[SIGQUIT SIGTERM SIGKILL].each do |signal| + it "has a worker that self-terminates on signal #{signal}" do + response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path) + expect(response.status).to eq(200) + + worker_pid = response.body.to_i + expect(worker_pid).to be > 0 + + begin + Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}") + rescue Excon::Error::Socket + # The connection may be closed abruptly + end + + expect(pid_gone?(worker_pid)).to eq(true) + end + end + + after(:all) do + WebMock.disable_net_connect!(allow_localhost: true) + Process.kill('TERM', @unicorn_master_pid) + end + + def wait_unicorn_boot!(master_pid, ready_file) + # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout. + timeout = 120 + timeout.times do + return if File.exist?(ready_file) + pid = Process.waitpid(master_pid, Process::WNOHANG) + raise "unicorn failed to boot: #{$?}" unless pid.nil? + + sleep 1 + end + + raise "unicorn boot timed out after #{timeout} seconds" + end + + def pid_gone?(pid) + # Worker termination should take less than a second. That makes 10 + # seconds a generous timeout. + 10.times do + begin + Process.kill(0, pid) + rescue Errno::ESRCH + return true + end + + sleep 1 + end + + false + end +end