Skip to content

Commit

Permalink
Better monitoring of forked Puma processes
Browse files Browse the repository at this point in the history
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`).
  • Loading branch information
marshall-lee committed Aug 2, 2022
1 parent 4bc6631 commit 11941fe
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 7 deletions.
17 changes: 10 additions & 7 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
end
70 changes: 70 additions & 0 deletions spec/support/fork.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 11941fe

Please sign in to comment.