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

(PUP-11897) Fix excessive cpu usage after EOF from an exec #9105

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/puppet/util/execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,12 @@ def self.execute(command, options = NoOptionsSpecified)
# Use non-blocking read to check for data. After each attempt,
# check whether the child is done. This is done in case the child
# forks and inherits stdout, as happens in `foo &`.

until results = Process.waitpid2(child_pid, Process::WNOHANG) #rubocop:disable Lint/AssignmentInCondition
# If we encounter EOF, though, then switch to a blocking wait for
# the child; after EOF, IO.select will never block and the loop
# below will use maximum CPU available.

wait_flags = Process::WNOHANG
until results = Process.waitpid2(child_pid, wait_flags) #rubocop:disable Lint/AssignmentInCondition

# If not done, wait for data to read with a timeout
# This timeout is selected to keep activity low while waiting on
Expand All @@ -235,6 +239,7 @@ def self.execute(command, options = NoOptionsSpecified)
output << reader.read_nonblock(4096) if ready
rescue Errno::EAGAIN
rescue EOFError
wait_flags &= ~Process::WNOHANG
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Masking off WNOHANG would make sense if something modified the variable between the time it was initialized to wait_flags = Process::WNOHANG and here. For example, if wait_flags was an in-and-out parameter in the call to Process.waitpid2. But since the variable isn't modified and the default value for the flags parameter is 0 (i.e. Process.waitpid2(pid, flags = 0)), then I think it'd be clearer to set this to wait_flags = 0.

end
end

Expand Down
13 changes: 13 additions & 0 deletions spec/integration/type/exec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@
end
end

context 'when an exec sends an EOF' do
let(:command) { ["/bin/bash", "-c", "exec /bin/sleep 1 >/dev/null 2>&1"] }

it 'should not take significant user time' do
exec = described_class.new :command => command, :path => ENV['PATH']
catalog.add_resource exec
timed_apply = Benchmark.measure { catalog.apply }
# In testing I found the user time before the patch in 4f35fd262e to be above
# 0.3, after the patch it was consistently below 0.1 seconds.
expect(timed_apply.utime).to be < 0.3
end
end

context 'when command is a string' do
let(:command) { "ruby -e 'File.open(\"#{path}\", \"w\") { |f| f.print \"foo\" }'" }

Expand Down
1 change: 1 addition & 0 deletions spec/unit/util/execution_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def stub_process_wait(exitstatus)
allow(FFI::WIN32).to receive(:CloseHandle).with(thread_handle)
else
allow(Process).to receive(:waitpid2).with(pid, Process::WNOHANG).and_return(nil, [pid, double('child_status', :exitstatus => exitstatus)])
allow(Process).to receive(:waitpid2).with(pid, 0).and_return(nil, [pid, double('child_status', :exitstatus => exitstatus)])
allow(Process).to receive(:waitpid2).with(pid).and_return([pid, double('child_status', :exitstatus => exitstatus)])
end
end
Expand Down