App-karr
view release on metacpan or search on metacpan
t/148-foundation-runner-child-leak.t view on Meta::CPAN
use strict;
use warnings;
# Ticket #147. App::karr::Foundation::Runner::_run_command owes a waitpid to
# every child it forks, and two calls that can throw sat between the two:
#
# 1. the parent opened .karr.log *after* the fork, so an unopenable log
# croaked with the agent already exec'd;
# 2. the TIMEOUT line was appended to that log *before* the SIGTERM/SIGKILL,
# so a log that went away during a run (the agent's own doing, e.g.)
# skipped the only thing that stops a hung agent.
#
# Neither is fatal to the process, which is what makes it expensive:
# _run_command is called from _drain_repo, which App::karr::Foundation's
# _process_repo wraps in try/catch ("drain error in $repo") and _release_lock's
# the board anyway. The run continues to the next board, so every affected board
# leaves a live, unwatched agent behind and looks unlocked to the next tick.
#
# The fork override has to be in place before the Runner is compiled, because a
# CORE::GLOBAL replacement only binds ops compiled after it -- so this lives in
# its own file, like t/122-foundation-runner-errors.t (which pins the wording of
# the log error under ticket #77).
our $LAST_CHILD_PID;
BEGIN {
*CORE::GLOBAL::fork = sub {
my $pid = CORE::fork();
$LAST_CHILD_PID = $pid if $pid; # true only in the parent; child sees 0
return $pid;
};
}
use Test::More;
use POSIX qw( WNOHANG );
use Path::Tiny qw( tempdir );
use App::karr::Foundation;
use App::karr::Foundation::Runner;
{
# Stands in for App::karr::Foundation with an _append_log that never
# throws. The real one writes the START line to the very file the subtest
# below breaks and would die there first -- which is why this window was
# hard to see, not why it was safe: it is the Runner's own open that has to
# happen before the fork.
package FakeFoundation;
sub new { bless {}, shift }
sub _stream_to_terminal { 0 }
sub _prompt_for { '' }
sub _append_log { }
sub _say_verbose { }
sub dry_run { 0 }
}
# Reap or kill whatever a regression leaves behind, so a failing run of this
# file does not also litter the machine. Only touches a pid that is still ours:
# once the library has reaped it, the number may belong to somebody else.
sub cleanup_child {
my ($pid) = @_;
return unless defined $pid;
return if waitpid( $pid, WNOHANG ) != 0;
kill 'KILL', $pid;
waitpid( $pid, 0 );
}
subtest 'an unopenable log is refused before anything is forked' => sub {
my $foundation = FakeFoundation->new; # held: the Runner keeps it weakly
my $runner = App::karr::Foundation::Runner->new( foundation => $foundation );
my $repo = tempdir( CLEANUP => 1 );
# A directory where the log belongs: open '>>' fails with EISDIR whatever
# the caller's privileges are, so this holds for root too.
$repo->child('.karr.log')->mkpath;
local $LAST_CHILD_PID;
eval { $runner->_run_command( $repo, { command => 'true', max_runtime => 5 } ) };
my $err = $@;
like $err, qr/^open log /, 'the run is refused, naming the log';
is $LAST_CHILD_PID, undef, 'and nothing was forked: no agent was started'
or diag "a child was forked before the log open failed: $LAST_CHILD_PID";
is waitpid( -1, WNOHANG ), -1, 'no child of this process is left over'
or diag 'the refused run left a child behind';
cleanup_child($LAST_CHILD_PID);
};
subtest 'a log lost mid-run does not cost the agent its SIGTERM and its reap' => sub {
# The reachable half: the log is writable when the run starts, so the
# foundation's START line lands and the agent is forked -- and then the
# agent itself replaces .karr.log with a directory and hangs past
# max_runtime. The TIMEOUT append then fails, in the window between the tee
# loop and the waitpid. `exec` in the command matters: without it /bin/sh
# keeps the sleep as a grandchild that survives the kill, and this test
# would litter the box.
my $f = App::karr::Foundation->new( _config_data => {} );
my $repo = tempdir( CLEANUP => 1 );
my $cmd = 'rm -f .karr.log; mkdir .karr.log; exec sleep 30';
local $LAST_CHILD_PID;
my @warnings;
my $ok = do {
local $SIG{__WARN__} = sub { push @warnings, $_[0] };
eval { $f->_run_command( $repo, { max_runtime => 1 }, $cmd ); 1 };
};
my $err = $@;
ok !$ok, 'the broken log still fails the run for this board'
or diag 'the run reported success over an unwritable log';
( run in 0.592 second using v1.01-cache-2.11-cpan-800906f7e73 )