FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

PROGRAM tasks always log exit_code=-1: type assertion performed on errors.Join wrapper instead of the exec error · Issue #827 · cybertec-postgresql/pg_timetable · GitHub

PROGRAM tasks always log exit_code=-1: type assertion performed on errors.Join wrapper instead of the exec error #827

Description

Summary

ExecuteProgramCommand() in internal/scheduler/shell.go type-asserts the accumulated error instead of the error just returned by the command when extracting the process exit code:

out, e := Cmd.CombinedOutput(ctx, command, params...)
if e != nil {
        exitCode = -1
        err = errors.Join(err, e)          // err becomes *errors.joinError (Join always wraps non-nil input)
        if exitError, ok := err.(*exec.ExitError); ok {   // asserts on the JOINED error, never succeeds
                exitCode = exitError.ExitCode()
        }
}

Because errors.Join(nil, e) wraps even a single non-nil error in its own *joinError type, the assertion err.(*exec.ExitError) can never succeed. Every failing program task is therefore logged with exit_code = -1, and the real exit status of the process (exitError.ExitCode()) is discarded.

Static-analysis finding against current master; not executed here.

Location

  • File: internal/scheduler/shell.go
  • Function: ExecuteProgramCommand() (~lines 55-62 of the loop body), consumed by sch.pgengine.LogTaskExecution(context.Background(), task, exitCode, ...) a few lines below.

Problem

Go's errors.Join returns a non-nil *joinError whenever at least one input error is non-nil - it does not pass through a lone wrapped error unchanged. After the first failing parameter set, err is a *joinError (or a join of several errors on later iterations), so:

  • err.(*exec.ExitError) fails on every iteration;
  • exitCode remains the pre-set -1;
  • LogTaskExecution() records -1 in timetable.execution_log for every failure, whether the underlying process exited with 1, 3, 127 or was killed.

The assertion was clearly meant to inspect e (the immediate error from this command run).

Trigger / Reproduction

Based on static analysis; no runtime run performed:

  1. Create a PROGRAM task whose command exits with a distinctive status, e.g. /bin/false (exit 1) or a script doing exit 3.
  2. Run it via any chain (single parameter value is enough).
  3. Observe timetable.execution_log: exit_code is recorded as -1 rather than 1/3.

Expected Behavior

The recorded exit code should reflect the actual process exit status when the failure is an *exec.ExitError, keeping -1 only for cases like signal termination where no exit code exists (and ideally distinguishing context-cancellation kills as well).

Actual Behavior

All failures are flattened to -1; real exit codes are lost for every execution.

Impact

Monitoring built on execution_log.exit_code cannot distinguish failure modes (e.g. "script validation failed with 3" vs "binary missing"), breaking alerting logic that matches specific codes. The accumulation intent across multiple parameter sets is preserved only for the final returned error, not for per-run logging.

Suggested Direction

Assert on e instead of err:

if e != nil {
        exitCode = -1
        var exitError *exec.ExitError
        if errors.As(e, &exitError) {
                exitCode = exitError.ExitCode()
        }
        err = errors.Join(err, e)
}

(errors.As also future-proofs against wrapped variants.)

Evidence

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL