App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Foundation/Runner.pm  view on Meta::CPAN

    unless (@ready) {
      # Spurious wakeup (signal) or genuine deadline. SIGALRM would have set
      # the flag, but the deadline could also be reached by wall clock if a
      # signal reset the alarm — check both and end the loop either way.
      next unless $max_runtime > 0;
      last if time - $started >= $max_runtime;
      next;
    }
    my $chunk;
    my $n = sysread( $reader, $chunk, 65536 );
    last if !defined $n;   # read error (or SIGALRM closing the fd)
    last if $n == 0;       # EOF — the command closed its output
    print {$log_fh} $chunk;
    if ($stream_terms) {
      $pending .= $chunk;
      print Encode::decode( 'UTF-8', $pending, Encode::FB_QUIET );
    }
    $output .= $chunk;
  }

  # Disarm the alarm before reap: a waitpid that takes longer than max_runtime
  # would otherwise be cut short by SIGALRM (no handler anymore — the default
  # action is to die, and Foundation is the parent). $max_runtime == 0 already
  # never armed.
  alarm 0;
  $SIG{ALRM} = 'DEFAULT' if $max_runtime > 0;

  my $exit_code;
  if ($timed_out) {
    my $elapsed = time - $started;
    # The one call that has to happen here rather than after the kill: it is the
    # only record of why the agent was stopped, and the kill/waitpid pair below
    # can block for as long as the child stays unkillable. So it runs
    # best-effort — a log the OS took away mid-run (#147) must not cost us the
    # SIGTERM/SIGKILL and the reap, which are all that stop a hung agent. The
    # failure is reported once the child is safely gone, and the END line below
    # raises it for real if the log is still unwritable by then.
    my $log_err;
    eval {
      $self->foundation->_append_log( $repo,
        "TIMEOUT after ${elapsed}s \x{2014} sending SIGTERM to $pid (group -$pid)" );
      1;
    } or $log_err = clean_error($@);
    # Negative pid = process group (kill(2) group semantics, #148). The shell,
    # the agent, any grandchildren the agent backgrounded, all receive the
    # signal. SIGTERM is catchable, so we wait up to 2s before escalating.
    kill 'TERM', -$pid;
    my $deadline = time + 2;
    while ( time < $deadline ) {
      last if kill( 0, $pid ) == 0;
      select undef, undef, undef, 0.05;
    }
    kill 'KILL', -$pid;
    waitpid( $pid, 0 );
    warn "karr-foundation: cannot write $log_file: $log_err\n" if $log_err;
    # 128 + SIGTERM(15) = 143 — same convention as shells, distinct from a
    # clean non-zero exit, and surfaces in cooldown/last_error so an agent
    # that exceeded max_runtime triggers the backoff (#164 / #161).
    $exit_code = 128 + SIGTERM;
  } else {
    # The child may still be alive after the loop ended on EOF — a command
    # whose stdout is closed while it keeps running (the classic
    # `exec >/dev/null 2>&1; sleep N`, #161). Reap it with a wait loop that
    # checks the wall-clock deadline: if the loop ended on EOF before
    # max_runtime expired, this blocks until the child exits on its own or
    # until the deadline arrives and we kill it via the timed_out path. The
    # loop uses WNOHANG to keep checking; the deadline path is identical to
    # the SIGALRM path above.
    my $deadline;
    if ( $max_runtime > 0 ) {
      $deadline = $started + $max_runtime;
      while (1) {
        my $w = waitpid( $pid, WNOHANG );
        last if $w > 0 || $w < 0;
        if ( time >= $deadline ) {
          $timed_out = 1;
          kill 'TERM', -$pid;
          my $term_deadline = time + 2;
          while ( time < $term_deadline ) {
            last if kill( 0, $pid ) == 0;
            select undef, undef, undef, 0.05;
          }
          kill 'KILL', -$pid;
          waitpid( $pid, 0 );
          last;
        }
        select undef, undef, undef, 0.05;
      }
    } else {
      waitpid( $pid, 0 );
    }
    $exit_code = _classify_exit($?);
    $exit_code = 128 + SIGTERM if $timed_out && $exit_code == 0;
  }

  close $reader if defined fileno $reader;
  close $log_fh;

  # Clear the live-agent handle: the SIGTERM handler must not see this agent
  # after we have reaped it. The next iteration of the drain (or the next
  # repo) installs its own.
  $self->foundation->_live_agent( undef );

  my $elapsed = time - $started;
  $self->foundation->_append_log( $repo, "END elapsed=${elapsed}s exit=$exit_code" );
  return ( $exit_code, $output );
}

# Translate the raw $? from waitpid(2) into the exit code the drain sees. A
# child that exited normally: the high 8 bits are the status. A child that
# died from a signal: the low 7 bits are the signal number and the high 8
# bits are 0 — `$? >> 8` was 0 here, which is the bug #164 pins: the runner
# reported the OOM-killed / SIGTERM'd / SIGSEGV'd agent as exit 0, the drain
# read it as a clean run, and the cooldown that was supposed to catch a
# machine-killing agent never engaged. Surface the signal as 128 + signum so
# it is distinguishable from any real exit code (shells do the same), and
# fall through to the normal high-bits path otherwise.
#
# Accept both forms: the runner calls this as a function
# (`_classify_exit($?)`) and tests call it as a method (`$r->_classify_exit($?)`).
# `use Moo;` turns every sub in the package into a method, so the method



( run in 1.651 second using v1.01-cache-2.11-cpan-14f38c9f855 )