Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detach bisect subprocesses to avoid making zombie processes #2739

Merged
merged 11 commits into from Aug 28, 2020
2 changes: 2 additions & 0 deletions Changelog.md
Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions lib/rspec/core/bisect/fork_runner.rb
Expand Up @@ -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
Expand Down
46 changes: 45 additions & 1 deletion spec/integration/bisect_spec.rb
Expand Up @@ -33,14 +33,58 @@ def bisect(cli_args, expected_status=nil)
end
end

context "when the bisect commasaturingnd is long" do
benoittgt marked this conversation as resolved.
Show resolved Hide resolved
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
it 'does not hit pipe size limit and does not get stuck' do
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
benoittgt marked this conversation as resolved.
Show resolved Hide resolved
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