Skip to content

Commit

Permalink
Add integration test for Puma worker reaping
Browse files Browse the repository at this point in the history
This test ensures that Puma handles the `Process.detach` bug described
in https://bugs.ruby-lang.org/issues/19837.
  • Loading branch information
stanhu committed Jan 26, 2024
1 parent f9233f1 commit d4ac708
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 2 deletions.
11 changes: 11 additions & 0 deletions test/config/process_detach_before_fork.rb
@@ -0,0 +1,11 @@
worker_shutdown_timeout 0

before_fork do
pid = fork do
sleep 30 # This has to exceed the test timeout
end

pid_filename = File.join(Dir.tmpdir, 'process_detach_test.pid')
File.write(pid_filename, pid)
Process.detach(pid)
end
32 changes: 30 additions & 2 deletions test/test_integration_cluster.rb
Expand Up @@ -175,6 +175,34 @@ def test_stuck_external_term_spawn
end
end

# From Ruby 2.6 to 3.2, `Process.detach` can delay or prevent
# `Process.wait2(-1)` from detecting a terminated child:
# https://bugs.ruby-lang.org/issues/19837. However,
# `Process.wait2(<child pid>)` still works properly. This bug has
# been fixed in Ruby 3.3.
def test_workers_respawn_with_process_detach
skip_unless_signal_exist? :KILL

config = 'test/config/process_detach_before_fork.rb'

worker_respawn(0, workers, config) do |phase0_worker_pids|
phase0_worker_pids.each do |pid|
Process.kill :KILL, pid
end
end

# `test/config/process_detach_before_fork.rb` forks and detaches a
# process. Since MiniTest attempts to join all threads before
# finishing, terminate the process so that the test can end quickly
# if it passes.
pid_filename = File.join(Dir.tmpdir, 'process_detach_test.pid')
if File.exist?(pid_filename)
pid = File.read(pid_filename).chomp.to_i
File.unlink(pid_filename)
Process.kill :TERM, pid if pid > 0
end
end

# mimicking stuck workers, test restart
def test_stuck_phased_restart
skip_unless_signal_exist? :USR1
Expand Down Expand Up @@ -681,10 +709,10 @@ def usr1_all_respond(unix: false, config: '')
end
end

def worker_respawn(phase = 1, size = workers)
def worker_respawn(phase = 1, size = workers, config = 'test/config/worker_shutdown_timeout_2.rb')
threads = []

cli_server "-w #{workers} -t 1:1 -C test/config/worker_shutdown_timeout_2.rb test/rackup/sleep_pid.ru"
cli_server "-w #{workers} -t 1:1 -C #{config} test/rackup/sleep_pid.ru"

# make sure two workers have booted
phase0_worker_pids = get_worker_pids
Expand Down

0 comments on commit d4ac708

Please sign in to comment.