From 11941fe579b325e68b9fba3e6897e5a1a0541ec4 Mon Sep 17 00:00:00 2001 From: Vladimir Kochnev Date: Tue, 2 Aug 2022 16:31:03 +0300 Subject: [PATCH] Better monitoring of forked Puma processes 3d3d5b3 introduced running Puma processes as child forks. The fix was necessary but it doesn't monitor the process state properly. What if `after(:suite)` block is not executed at all and child process becomes a zombie? What if fork process refuses to exit for some reason? So I have introduced a `Fork` helper that tries to prevent forks from becoming a zombie by sending it `SIGKILL` if `SIGTERM` didn't have an effect in 500ms or when Ruby interpreter terminates completely (`at_exit`). --- spec/spec_helper.rb | 17 ++++++----- spec/support/fork.rb | 70 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 spec/support/fork.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d3c0315..865ecf8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,14 +18,17 @@ Dir['./spec/support/**/*.rb'].each { |fn| require fn } -http_server_pid = Process.fork { PatronTestServer.start(false, 9001) } -https_server_pid = Process.fork { PatronTestServer.start(true, 9043) } +http_server = Fork.new { PatronTestServer.start(false, 9001) } + +sleep 0.1 # Don't interfere the start up output of two processes. + +https_server = Fork.new { PatronTestServer.start(true, 9043) } RSpec.configure do |c| c.after(:suite) do - Process.kill("INT", http_server_pid) - Process.kill("INT", https_server_pid) - Process.wait(http_server_pid) - Process.wait(https_server_pid) + http_server.kill("TERM") + https_server.kill("TERM") + http_server.wait + https_server.wait end -end \ No newline at end of file +end diff --git a/spec/support/fork.rb b/spec/support/fork.rb new file mode 100644 index 0000000..5661dd5 --- /dev/null +++ b/spec/support/fork.rb @@ -0,0 +1,70 @@ +require 'thread' + +class Fork + def initialize(&block) + @mu = Mutex.new + @cond = ConditionVariable.new + @pstate = nil + @pid = Process.fork(&block) + @killed = false + + # Start monitoring the PID. + Thread.new { monitor } + + # Kill the process anyway when the program exits. + ppid = Process.pid + at_exit do + if ppid == Process.pid # Make sure we are not inside another fork spawned by rspec example. + do_kill("KILL") + end + end + end + + # Wait for process to exit. + def wait(timeout = nil) + @mu.synchronize do + next @pstate unless @pstate.nil? + + @cond.wait(@mu, timeout) + @pstate + end + end + + # Signal the process. + def kill(sig) + already_killed = @mu.synchronize do + old = @killed + @killed = true + old + end + signaled = do_kill(sig) + Thread.new { reaper } if signaled && !already_killed + signaled + end + + private + + # Signal the process. + def do_kill(sig) + Process.kill(sig, @pid) + true + rescue Errno::ESRCH # No such process + false + end + + # Monitor the process state. + def monitor + _, pstate = Process.wait2(@pid) + + @mu.synchronize do + @pstate = pstate + @cond.broadcast + end + end + + # Wait 500 milliseconds and force terminate. + def reaper + pstate = wait(0.5) + do_kill("KILL") unless pstate + end +end