diff --git a/Changelog.md b/Changelog.md index 5e3f84d20a..020b853dc2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,8 @@ Bug Fixes: * Ensure custom error codes are returned from bisect runs. (Jon Rowe, #2732) * Ensure `RSpec::Core::Configuration` predicate config methods return booleans. (Marc-André Lafortune, #2736) +* Prevent `rspec --bisect` from generating zombie processes while executing + bisect runs. (Benoit Tigeot, Jon Rowe, #2739) ### 3.9.2 / 2020-05-02 [Full Changelog](http://github.com/rspec/rspec-core/compare/v3.9.1...v3.9.2) diff --git a/lib/rspec/core/bisect/fork_runner.rb b/lib/rspec/core/bisect/fork_runner.rb index b772e81af3..4641a14429 100644 --- a/lib/rspec/core/bisect/fork_runner.rb +++ b/lib/rspec/core/bisect/fork_runner.rb @@ -91,9 +91,12 @@ def initialize(runner, channel) end def dispatch_specs(run_descriptor) - fork { run_specs(run_descriptor) } + pid = fork { run_specs(run_descriptor) } # We don't use Process.waitpid here as it was causing bisects to - # block due to the file descriptor limit on OSX / Linux. + # block due to the file descriptor limit on OSX / Linux. We need + # to detach the process to avoid having zombie processes + # consuming slots in the kernel process table during bisect runs. + Process.detach(pid) end private diff --git a/spec/integration/bisect_spec.rb b/spec/integration/bisect_spec.rb index 1d3a864391..820ca4d8c9 100644 --- a/spec/integration/bisect_spec.rb +++ b/spec/integration/bisect_spec.rb @@ -33,7 +33,7 @@ def bisect(cli_args, expected_status=nil) end end - context "when the bisect commasaturingnd is long" do + context "when the bisect command saturates the pipe" do # On OSX and Linux a file descriptor limit meant that the bisect process got stuck at a certain limit. # This test demonstrates that we can run large bisects above this limit (found to be at time of commit). # See: https://github.com/rspec/rspec-core/pull/2669 @@ -41,6 +41,50 @@ def bisect(cli_args, expected_status=nil) output = bisect(%W[spec/rspec/core/resources/blocking_pipe_bisect_spec.rb_], 1) expect(output).to include("No failures found.") end + + it 'does not leave zombie processes', :unless => RSpec::Support::OS.windows? do + bisect(['--format', 'json', 'spec/rspec/core/resources/blocking_pipe_bisect_spec.rb_'], 1) + + zombie_process = RSpecChildProcess.new(Process.pid).zombie_process + expect(zombie_process).to eq([]), <<-MSG + Expected no zombie processes got #{zombie_process.count}: + #{zombie_process} + MSG + end + end + + class RSpecChildProcess + Ps = Struct.new(:pid, :ppid, :state, :command) + + def initialize(pid) + @list = child_process_list(pid) + end + + def zombie_process + @list.select { |child_process| child_process.state =~ /Z/ } + end + + private + + def child_process_list(pid) + childs_process_list = [] + ps_pipe = `ps -o pid=,ppid=,state=,args= | grep #{pid}` + + ps_pipe.split(/\n/).map do |line| + ps_part = line.lstrip.split(/\s+/) + + next unless ps_part[1].to_i == pid + + child_process = Ps.new + child_process.pid = ps_part[0] + child_process.ppid = ps_part[1] + child_process.state = ps_part[2] + child_process.command = ps_part[3..-1].join(' ') + + childs_process_list << child_process + end + childs_process_list + end end end end