Skip to content

Report timeout as structured error instead of deserialization failure#75

Open
twolff-gh wants to merge 1 commit into
openshift-eng:mainfrom
twolff-gh:improve-timeout-error-reporting
Open

Report timeout as structured error instead of deserialization failure#75
twolff-gh wants to merge 1 commit into
openshift-eng:mainfrom
twolff-gh:improve-timeout-error-reporting

Conversation

@twolff-gh

@twolff-gh twolff-gh commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

https://redhat.atlassian.net/browse/ARO-25369

When a test subprocess times out and is killed, the parent can't parse its output and reports a generic "Deserialization Error" indistinguishable from a non-timeout crash.

This branch tracks whether the signal escalation goroutine fired due to a timeout using an atomic flag (set before SIGINT to avoid a race), and when it did, replaces the generic error with "test timed out after ; subprocess exit: " so CI tools like Sippy can identify timeout failures.

Summary by CodeRabbit

Summary

  • Bug Fixes

    • Improved tracking of timeout escalation for long-running processes, ensuring consistent timeout/timing behavior.
    • Enhanced failure diagnostics when escalation occurs by including timeout-specific stderr details.
    • Preserved successfully parsed results even if escalation happened; improved handling and reporting when output parsing fails.
  • Tests

    • Added unit tests covering output parsing, result construction (timing/output/error), grace period validation, and timeout-related error messaging.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 25, 2026
@openshift-ci

openshift-ci Bot commented Jun 25, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

SpawnProcessToRunTest now tracks timeout escalation atomically, preserves the SIGINT-to-SIGABRT sequence, parses subprocess output after waiting, and returns timeout-aware failure results when parsing fails.

Changes

Subprocess timeout and result handling

Layer / File(s) Summary
Timeout escalation and result reporting
pkg/ginkgo/parallel.go
Parent timeout handling records escalation, signals the subprocess, parses stdout after completion, and differentiates timeout failures from command or deserialization errors.
Result parsing and timeout contract tests
pkg/ginkgo/parallel_test.go
Tests cover JSON parsing, result construction, error summarization, grace-period timing, and timeout message contents.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SpawnProcessToRunTest
  participant ChildProcess
  participant ResultParser
  SpawnProcessToRunTest->>ChildProcess: Start and wait for test subprocess
  SpawnProcessToRunTest->>ChildProcess: Send SIGINT, then SIGABRT after escalation
  ChildProcess-->>SpawnProcessToRunTest: Exit status and stdout
  SpawnProcessToRunTest->>ResultParser: Parse stdout
  ResultParser-->>SpawnProcessToRunTest: Parsed result or parse error
Loading

Possibly related PRs

Suggested reviewers: deads2k, stbenjam

🚥 Pre-merge checks | ✅ 9 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Ai-Attribution ⚠️ Warning Commit uses Co-Authored-By: Claude..., but no Assisted-by/Generated-by Red Hat trailer is present. Replace the AI co-author trailer with Red Hat-compliant Assisted-by or Generated-by attribution.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: timeout handling now returns a structured error instead of a deserialization failure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Weak-Crypto ✅ Passed No weak crypto primitives, custom crypto, or secret/token comparisons were introduced in the touched files.
Container-Privileges ✅ Passed PR only changes Go code in pkg/ginkgo; no container/K8s manifests or privilege settings (privileged, hostPID, hostNetwork, hostIPC, allowPrivilegeEscalation, runAsUser:0) were added.
No-Sensitive-Data-In-Logs ✅ Passed The new logging only emits timeout duration, exit status, and generic error text; I found no secrets, PII, API keys, or hostnames in the changed code.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, embedded credentials, private keys, or suspicious secret literals were found in the touched files.
No-Injection-Vectors ✅ Passed No SQL/shell/eval/yaml/pickle injection sinks were added; the new exec.CommandContext call passes args directly, not via a shell.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/ginkgo/parallel.go (1)

48-65: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Set timedOut before sending SIGINT to close a timing race.

timedOut.Store(true) (Line 55) runs after the SIGINT is sent (Line 49). The main goroutine reads timedOut.Load() (Line 65) immediately after command.Wait() returns. If the subprocess exits in response to SIGINT and Wait() returns before the goroutine reaches the Store, timeout_fired reads false and the timeout is misreported as a generic parse/command failure. Move the store ahead of the signal (after the Canceled check, which already distinguishes the normal-exit case).

🔒️ Proposed reorder to remove the race window
 		if command.Process != nil {
-			_ = command.Process.Signal(syscall.SIGINT)
+			// no-op; signal sent below after recording the timeout
 		}
 		// Canceled means the process exited and the context was cancelled — no need to escalate
 		if timeoutCtx.Err() == context.Canceled {
 			return
 		}
 		timedOut.Store(true)
+		if command.Process != nil {
+			_ = command.Process.Signal(syscall.SIGINT)
+		}
 		// if the process is hung, send SIGABRT after a grace period for a stack dump
 		<-time.After(time.Minute)
 		if command.Process != nil {
 			_ = command.Process.Signal(syscall.SIGABRT)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel.go` around lines 48 - 65, The timeout race in the
parallel command cleanup logic should be fixed by setting timedOut before
sending SIGINT. Update the goroutine in parallel.go so timedOut.Store(true)
happens immediately after the timeoutCtx.Err() == context.Canceled check and
before command.Process.Signal(syscall.SIGINT), then keep the SIGABRT escalation
path unchanged; this ensures the main goroutine’s timedOut.Load() after
command.Wait() cannot miss the timeout signal.
🧹 Nitpick comments (2)
pkg/ginkgo/parallel.go (2)

65-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nit: prefer idiomatic camelCase.

timeout_fired is snake_case; Go convention is timeoutFired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel.go` at line 65, The local variable in parallel.go uses
snake_case instead of Go’s idiomatic camelCase. Rename timeout_fired to
timeoutFired at the assignment and all nearby uses in the same scope so the
naming matches Go conventions and remains consistent with the timedOut.Load()
logic.

121-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Optional: reduce duplication with newTestResult.

newTimeoutResult re-implements the duration/DBTime/Output construction already in newTestResult. Consider delegating the base result to newTestResult and overriding only Error to keep the two in sync as the struct evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel.go` around lines 121 - 142, newTimeoutResult duplicates
the base result-building logic already present in newTestResult, so keep the two
paths in sync by delegating shared fields to newTestResult and only customizing
the timeout-specific Error message. Update the newTimeoutResult function to
reuse the Duration, StartTime, EndTime, and Output construction from
newTestResult, using the same ExtensionTestResult structure and identifiers so
future changes to result shaping only need to be made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pkg/ginkgo/parallel.go`:
- Around line 48-65: The timeout race in the parallel command cleanup logic
should be fixed by setting timedOut before sending SIGINT. Update the goroutine
in parallel.go so timedOut.Store(true) happens immediately after the
timeoutCtx.Err() == context.Canceled check and before
command.Process.Signal(syscall.SIGINT), then keep the SIGABRT escalation path
unchanged; this ensures the main goroutine’s timedOut.Load() after
command.Wait() cannot miss the timeout signal.

---

Nitpick comments:
In `@pkg/ginkgo/parallel.go`:
- Line 65: The local variable in parallel.go uses snake_case instead of Go’s
idiomatic camelCase. Rename timeout_fired to timeoutFired at the assignment and
all nearby uses in the same scope so the naming matches Go conventions and
remains consistent with the timedOut.Load() logic.
- Around line 121-142: newTimeoutResult duplicates the base result-building
logic already present in newTestResult, so keep the two paths in sync by
delegating shared fields to newTestResult and only customizing the
timeout-specific Error message. Update the newTimeoutResult function to reuse
the Duration, StartTime, EndTime, and Output construction from newTestResult,
using the same ExtensionTestResult structure and identifiers so future changes
to result shaping only need to be made in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift-eng/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 18020dbc-d317-415a-b09d-720051f4b552

📥 Commits

Reviewing files that changed from the base of the PR and between 220cea4 and 1abddee.

📒 Files selected for processing (1)
  • pkg/ginkgo/parallel.go

@twolff-gh
twolff-gh force-pushed the improve-timeout-error-reporting branch from 1abddee to 6b69cdc Compare June 26, 2026 19:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/ginkgo/parallel.go`:
- Around line 66-68: The early return in newTestResultFromOutput currently
bypasses timeout annotation when stdout parses successfully, so a timed-out
subprocess can return an ExtensionTestResult without the expected Error context.
Update newTestResultFromOutput in parallel.go to preserve the timeout
information on the successful parse path by checking the timeout state before
returning subcommandResult and setting ExtensionTestResult.Error consistently so
pkg/ginkgo/util.go can consume the expected contract.
- Around line 57-60: The delayed SIGABRT in the parallel process watcher can
still fire after Wait() returns, since the goroutine is never stopped and
command.Process may remain non-nil. Update the logic in parallel.go around the
goroutine that sends SIGTERM then waits before SIGABRT by adding a done channel
or cancellable context that is closed when Wait() completes, and gate the second
signal send on that cancellation before calling command.Process.Signal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift-eng/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6c52ba71-23d7-417a-a57b-c436ea59e3b9

📥 Commits

Reviewing files that changed from the base of the PR and between 1abddee and 6b69cdc.

📒 Files selected for processing (1)
  • pkg/ginkgo/parallel.go

Comment thread pkg/ginkgo/parallel.go
Comment thread pkg/ginkgo/parallel.go
@twolff-gh
twolff-gh force-pushed the improve-timeout-error-reporting branch from 6b69cdc to 310568b Compare June 26, 2026 21:29
@twolff-gh
twolff-gh marked this pull request as ready for review June 26, 2026 21:47
@openshift-ci openshift-ci Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jun 26, 2026
@openshift-ci
openshift-ci Bot requested review from deads2k and stbenjam June 26, 2026 21:47
@roivaz

roivaz commented Jul 15, 2026

Copy link
Copy Markdown

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 15, 2026
@openshift-ci

openshift-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: roivaz, twolff-gh
Once this PR has been reviewed and has the lgtm label, please assign smg247 for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@not-stbenjam not-stbenjam left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for this.

One thing I noticed while looking at the timing here (pre-existing, not something this PR introduced): the child process gets --timeout passed to it and is supposed to enforce its own deadline, but the parent's escalation timer starts ticking before the child's. That means the parent will always fire first, SIGINT the child, and the child never gets the chance to time out on its own. The child's self-reported timeout is the much nicer outcome, because ginkgo knows exactly which spec timed out and where, and it emits proper JSON that we can parse.

I think the fix is to give the parent a grace period, rather than exactly timeout, so the child gets first crack at reporting its own timeout, and the parent's escalation becomes the backstop for truly hung processes.

Some unit or integration tests around this would be great as well.

@stbenjam

stbenjam commented Jul 15, 2026

Copy link
Copy Markdown
Member

Claude's comment above ^

I can take a look next week but the feedback looks reasonable

@twolff-gh

Copy link
Copy Markdown
Contributor Author

Thanks for this.

One thing I noticed while looking at the timing here (pre-existing, not something this PR introduced): the child process gets --timeout passed to it and is supposed to enforce its own deadline, but the parent's escalation timer starts ticking before the child's. That means the parent will always fire first, SIGINT the child, and the child never gets the chance to time out on its own. The child's self-reported timeout is the much nicer outcome, because ginkgo knows exactly which spec timed out and where, and it emits proper JSON that we can parse.

I think the fix is to give the parent a grace period, rather than exactly timeout, so the child gets first crack at reporting its own timeout, and the parent's escalation becomes the backstop for truly hung processes.

Some unit or integration tests around this would be great as well.

Sounds good. Ill factor that in. Ill also add tests 👍
Thank you!

@openshift-ci openshift-ci Bot removed the lgtm Indicates that a PR is ready to be merged. label Jul 15, 2026
@openshift-ci

openshift-ci Bot commented Jul 15, 2026

Copy link
Copy Markdown

New changes are detected. LGTM label has been removed.

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/ginkgo/parallel_test.go (2)

71-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the specific error message to catch a formatting bug.

Consider asserting the exact error string returned here, similar to the empty buffer test. Doing so will expose a bug in the upstream newTestResultFromOutput function (visible in the provided context snippets), where errors.New("expected 1 result, got %v results") is incorrectly used instead of fmt.Errorf.

♻️ Proposed refactor
 	_, err := newTestResultFromOutput(stdout)
 	if err == nil {
 		t.Fatal("expected error for multi-element array, got nil")
 	}
+	expectedErr := "expected 1 result, got %v results" // Note: This matches the upstream formatting bug
+	if err.Error() != expectedErr {
+		t.Errorf("error = %q, want %q", err.Error(), expectedErr)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel_test.go` around lines 71 - 75, Update the multi-element
array case in the newTestResultFromOutput test to assert the exact expected
error message, including the actual result count, rather than only checking that
an error occurred. Use the same error-string assertion pattern as the
empty-buffer test so the formatting behavior of newTestResultFromOutput is
verified.

187-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid testing fmt.Sprintf with a duplicated format string.

This test verifies the output of fmt.Sprintf using a format string copied directly into the test. This approach does not guarantee that SpawnProcessToRunTest actually uses this exact format. Consider extracting the format string into a shared constant so both the test and the implementation use the same source of truth, or test the output of the function directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel_test.go` around lines 187 - 201, Update
TestTimeoutErrorMessage to exercise the production timeout-message path,
preferably by invoking SpawnProcessToRunTest with controlled timeout and
subprocess-error inputs instead of duplicating fmt.Sprintf. Alternatively,
extract the format string into a shared named constant used by both
SpawnProcessToRunTest and the test, then keep assertions focused on the
resulting message contents.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/ginkgo/parallel_test.go`:
- Line 19: Handle the json.Marshal errors in pkg/ginkgo/parallel_test.go at
lines 19, 38, 54, and 68 by capturing each error and failing the corresponding
test immediately with t.Fatalf when marshaling fails; preserve the existing data
flow after successful marshaling.

---

Nitpick comments:
In `@pkg/ginkgo/parallel_test.go`:
- Around line 71-75: Update the multi-element array case in the
newTestResultFromOutput test to assert the exact expected error message,
including the actual result count, rather than only checking that an error
occurred. Use the same error-string assertion pattern as the empty-buffer test
so the formatting behavior of newTestResultFromOutput is verified.
- Around line 187-201: Update TestTimeoutErrorMessage to exercise the production
timeout-message path, preferably by invoking SpawnProcessToRunTest with
controlled timeout and subprocess-error inputs instead of duplicating
fmt.Sprintf. Alternatively, extract the format string into a shared named
constant used by both SpawnProcessToRunTest and the test, then keep assertions
focused on the resulting message contents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift-eng/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 07590443-6e20-47b6-b11e-1c62b7c072a6

📥 Commits

Reviewing files that changed from the base of the PR and between 6ddc110 and 358f7f1.

📒 Files selected for processing (2)
  • pkg/ginkgo/parallel.go
  • pkg/ginkgo/parallel_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/ginkgo/parallel.go

Comment thread pkg/ginkgo/parallel_test.go Outdated
Name: "test-single",
Result: extensiontests.ResultPassed,
}
data, _ := json.Marshal(result)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not ignore error returns.

The error returns from json.Marshal are being intentionally ignored, which violates the path instructions for Go files. In test code, silently suppressing these errors can lead to confusing test failures down the line (e.g., passing empty buffers into the function under test).

  • pkg/ginkgo/parallel_test.go#L19-L19: Handle the error from json.Marshal(result).
  • pkg/ginkgo/parallel_test.go#L38-L38: Handle the error from json.Marshal(results).
  • pkg/ginkgo/parallel_test.go#L54-L54: Handle the error from json.Marshal([]extensiontests.ExtensionTestResult{}).
  • pkg/ginkgo/parallel_test.go#L68-L68: Handle the error from json.Marshal(results).

As per path instructions, "Never ignore error returns". Please assign the error to a variable and assert that it is nil (e.g., using t.Fatalf("failed to marshal JSON: %v", err)).

📍 Affects 1 file
  • pkg/ginkgo/parallel_test.go#L19-L19 (this comment)
  • pkg/ginkgo/parallel_test.go#L38-L38
  • pkg/ginkgo/parallel_test.go#L54-L54
  • pkg/ginkgo/parallel_test.go#L68-L68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ginkgo/parallel_test.go` at line 19, Handle the json.Marshal errors in
pkg/ginkgo/parallel_test.go at lines 19, 38, 54, and 68 by capturing each error
and failing the corresponding test immediately with t.Fatalf when marshaling
fails; preserve the existing data flow after successful marshaling.

Source: Path instructions

@twolff-gh
twolff-gh force-pushed the improve-timeout-error-reporting branch from 358f7f1 to fc864a9 Compare July 16, 2026 14:46
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 16, 2026
The parent's escalation timer now waits an extra 2 minutes beyond the
child's --timeout before sending SIGINT. This lets the child's
Ginkgo-level timeout fire first, producing structured per-spec output
instead of the parent killing it prematurely.

Also reorders the signal goroutine to check context.Canceled before
sending SIGINT (avoids spurious signal to already-exited processes)
and improves the fallback error wording.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@twolff-gh
twolff-gh force-pushed the improve-timeout-error-reporting branch from abe47bc to 5a15ed0 Compare July 17, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants