Skip to content

fix: retry transient network errors fetching control plane version graph (AROSLSRE-2030) - #6828

Merged
openshift-merge-bot[bot] merged 2 commits into
Azure:mainfrom
raelga:rael/aroslsre-2030-cincinnati-retry
Sep 9, 2026
Merged

fix: retry transient network errors fetching control plane version graph (AROSLSRE-2030)#6828
openshift-merge-bot[bot] merged 2 commits into
Azure:mainfrom
raelga:rael/aroslsre-2030-cincinnati-retry

Conversation

@raelga

Copy link
Copy Markdown
Collaborator

AROSLSRE-2030

What

Retries transient failures (DNS/dial timeouts, connection resets, 5xx) with exponential backoff when fetching the control plane version graph from the Cincinnati update service, instead of failing on the first hiccup.

Why

E2E build 2095800805629104128 failed the Update HCPOpenShiftCluster test with:

failed to get latest install version for candidate channel: failed getting controlPlaneVersion:
Get "https://api.openshift.com/api/upgrades_info/graph?arch=multi&channel=candidate-4.21":
dial tcp: lookup api.openshift.com on 172.30.0.10:53: read udp 10.129.222.58:38094->172.30.0.10:53: i/o timeout

A single DNS lookup timeout on the test pod's cluster DNS failed the whole run. SelectControlPlaneVersion made one unretried HTTP GET, so any transient network blip was fatal.

Testing

  • go build ./backend/...
  • go test ./backend/...

A genuine 4xx response from the update service still fails immediately with no retry; existing unit tests for SelectControlPlaneVersion mock a single 200 response and pass unchanged.

Special notes for your reviewer

Backoff is 1s doubling up to a 30s cap over 5 attempts, using k8s.io/apimachinery/pkg/util/wait (already a dependency elsewhere in the repo).

PR Checklist

  • Tests pass
  • Documentation not required (no user-facing behavior change)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new retry helper currently masks context cancellation and discards the underlying retryable error details, reducing diagnosability and potentially changing cancellation semantics.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves the reliability of backend control plane version selection by adding retry logic when fetching the Cincinnati update graph, reducing E2E flakes caused by transient network issues.

Changes:

  • Adds exponential-backoff retries around the HTTP GET to the Cincinnati graph endpoint.
  • Treats transport errors and 5xx responses as retryable, while failing fast on non-200 non-5xx responses.
  • Introduces a new doWithRetry helper to encapsulate this retry behavior.
File summaries
File Description
backend/pkg/controllers/controlplaneversion/cincinnati.go Wraps Cincinnati graph fetch with exponential-backoff retry logic via a new helper function.
Review details

Suppressed comments (2)

backend/pkg/controllers/controlplaneversion/cincinnati.go:156

  • The error handling here can mask context cancellation/timeouts and drops the underlying wait error details by replacing it with a generic message. Other retry helpers in this repo return ctx.Err() when the parent context is canceled and wrap the backoff error for diagnosability.
	if err != nil {
		if wait.Interrupted(err) {
			return nil, fmt.Errorf("%s did not respond successfully after retries", req.URL)
		}

backend/pkg/controllers/controlplaneversion/cincinnati.go:135

  • Transport-level errors are treated as retryable but the specific error is discarded (returning (false, nil)). If retries are exhausted, callers won’t see the last underlying network/TLS/DNS error, which makes diagnosing persistent failures harder. Track the last retryable error/status and return it when wait.Interrupted(err) is hit (similar to tooling/templatize/pkg/pipeline/arm_retry_policy.go and slot-manager lease proxy retry).
		resp, err := client.Do(req.Clone(ctx))
		if err != nil {
			// Treat any transport-level error (DNS lookup failures, dial
			// timeouts, connection resets, etc.) as transient and retry.
			return false, nil
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go Outdated
@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 8bfec2c to 760d12b Compare September 4, 2026 12:46
Copilot AI review requested due to automatic review settings September 4, 2026 12:46
@raelga

Copy link
Copy Markdown
Collaborator Author

Force-pushed a fix for the `ci/prow/lint` failure: `gci` flagged `backend/pkg/controllers/controlplaneversion/cincinnati.go` for import grouping, the `k8s.io/apimachinery/pkg/util/wait` import needs its own group before the `github.com/openshift` group per `.golangci.yml`'s custom-order sections. Reordered the import block accordingly.

Verified locally with `golangci-lint run --build-tags='E2Etests' ./backend/...` (0 issues) and `go build ./backend/...` / `go test ./backend/pkg/controllers/controlplaneversion/...` (pass).

The `Analyze (go)` CodeQL check failure on the previous push was `The runner has received a shutdown signal`, an unrelated GitHub Actions runner infra hiccup, not caused by this diff.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new retry path currently discards the underlying failure details (transport errors / 5xx statuses), reducing debuggability and making exhausted-retry errors non-actionable.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

backend/pkg/controllers/controlplaneversion/cincinnati.go:143

  • On 5xx responses doWithRetry retries but doesn’t preserve the status for the final error. Capturing the last seen 5xx (and draining the body) improves debuggability and helps HTTP connection reuse under retry.
		if resp.StatusCode >= http.StatusInternalServerError {
			// Server-side errors are typically transient; retry.
			return false, nil
		}

backend/pkg/controllers/controlplaneversion/cincinnati.go:158

  • When retries are exhausted, the returned error loses the root cause (last transport error / last 5xx status). Wrapping the final error with the last observed failure makes this actionable in logs and test output.
	if err != nil {
		if wait.Interrupted(err) {
			return nil, fmt.Errorf("%s did not respond successfully after retries", req.URL)
		}
		return nil, err
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go
@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 760d12b to 0505a5d Compare September 4, 2026 12:50
Copilot AI review requested due to automatic review settings September 4, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The retry implementation currently loses useful error context / cancellation semantics in some paths and should be adjusted for debuggability and correct retry behavior before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

backend/pkg/controllers/controlplaneversion/cincinnati.go:162

  • The retry timeout/cancellation error handling drops context: when ExponentialBackoffWithContext returns an interruption/timeout, this wraps it into a generic message and loses the underlying error (and can also hide ctx cancellation/deadline). Returning ctx.Err() when set and wrapping the interruption error keeps debugging information without changing retry behavior.
	if err != nil {
		if wait.Interrupted(err) {
			return nil, fmt.Errorf("%s did not respond successfully after retries", req.URL)
		}
		return nil, err

backend/pkg/controllers/controlplaneversion/cincinnati.go:147

  • When retrying on 5xx responses, the response body is closed but not drained; this prevents HTTP connection reuse and can amplify transient failures by forcing new TCP/TLS handshakes on each retry. Drain a bounded amount of the body before retrying.
		if resp.StatusCode >= http.StatusInternalServerError {
			// Server-side errors are typically transient; retry.
			return false, nil
		}
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go
Comment thread backend/pkg/controllers/controlplaneversion/cincinnati_retry_test.go Outdated
Copilot AI review requested due to automatic review settings September 4, 2026 12:55
@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 0505a5d to 895e8f8 Compare September 4, 2026 12:55
@raelga

Copy link
Copy Markdown
Collaborator Author

Pushed a follow-up commit addressing Copilot's two review comments:

  1. Injectable backoff - doWithRetry now takes a wait.Backoff parameter instead of hard-coding one, with a package-level cincinnatiRetryBackoff var as the production default. Added cincinnati_retry_test.go with a fast test backoff (1ms/3 steps) covering: retry-on-transport-error, retry-on-5xx, no-retry-on-4xx, and backoff-exhaustion.
  2. Dropped underlying error / context handling - doWithRetry now preserves the last transport/5xx error and wraps it into the final error when the backoff is exhausted, and returns ctx.Err() immediately (no more retries) when the caller's context is cancelled or times out, instead of masking it as a generic transport error. Added two more subtests covering these.

Verified locally: go build ./backend/..., go test ./backend/pkg/controllers/controlplaneversion/... (all pass), golangci-lint run (0 issues).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new retry logic should drain non-200 response bodies for connection reuse and should avoid retrying clearly non-transient transport errors to match the stated intent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go Outdated
Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go
Copilot AI review requested due to automatic review settings September 4, 2026 12:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The retry/backoff behavior is well-contained, matches the stated failure mode, and is covered by targeted unit tests; remaining feedback is a minor performance optimization.

Review details

Suppressed comments (2)

backend/pkg/controllers/controlplaneversion/cincinnati.go:157

  • On 5xx responses you close the response body without draining it. In net/http that typically prevents connection reuse, so repeated retries can open a new TCP/TLS connection per attempt. Consider draining the body before retrying to allow keep-alive reuse.
		if resp.StatusCode >= http.StatusInternalServerError {
			// Server-side errors are typically transient; retry.
			lastErr = fmt.Errorf("%s returned unexpected HTTP status %s", req.URL, resp.Status)
			return false, nil

backend/pkg/controllers/controlplaneversion/cincinnati.go:160

  • For non-200 non-5xx responses you also return without draining the body, which typically prevents HTTP connection reuse for subsequent calls. Draining before returning the error can reduce connection churn.
		if resp.StatusCode != http.StatusOK {
			return false, fmt.Errorf("%s returned unexpected HTTP status %s", req.URL, resp.Status)
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 4, 2026 13:04
@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 4cdbcf1 to 24a90aa Compare September 4, 2026 13:04
@raelga

Copy link
Copy Markdown
Collaborator Author

One more follow-up push addressing two more Copilot review comments:

  1. Narrowed retry classification - doWithRetry now only retries transport errors that look genuinely transient (net.DNSError, net.Error timeouts, ECONNRESET/ECONNREFUSED/ETIMEDOUT, unexpected EOF). Anything else (bad URL, unsupported scheme, TLS cert errors, etc.) fails fast on the first attempt instead of being retried and delaying the eventual failure.
  2. Body draining + status formatting - non-200 responses now drain the body before closing so the underlying connection can be reused on retry, and the error message uses resp.StatusCode instead of resp.Status (which can be empty on synthetic responses).

Verified locally: go build ./backend/..., go test ./backend/pkg/controllers/controlplaneversion/... (all pass, added a fail-fast-on-non-transient-error subtest), golangci-lint run (0 issues).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The retry behavior is correctly implemented and covered by unit tests, with only a minor comment wording nit remaining.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go Outdated
Copilot AI review requested due to automatic review settings September 4, 2026 13:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

DNS errors are currently treated as transient unconditionally, which can cause retries/backoff on permanent DNS failures (e.g., NXDOMAIN) and unintentionally delay fail-fast behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

backend/pkg/controllers/controlplaneversion/cincinnati.go:216

  • For net.Error, only Timeout() is treated as transient. Many transient network errors are reported as Temporary() without being timeouts; including Temporary() would better match the goal of retrying transient transport failures while still failing fast for non-transient errors.
	var netErr net.Error
	if errors.As(err, &netErr) {
		return netErr.Timeout()
	}
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go
Copilot AI review requested due to automatic review settings September 4, 2026 13:13
@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 49c8874 to 982e792 Compare September 4, 2026 13:13
@raelga

Copy link
Copy Markdown
Collaborator Author

Fixed the NXDOMAIN edge case from Copilot's last comment: isTransientTransportError now only retries a *net.DNSError when IsTimeout or IsTemporary is set, so permanent failures (NXDOMAIN / "no such host") fail fast instead of adding ~15s of backoff first. Added a subtest for it. Verified: build, go test ./backend/pkg/controllers/controlplaneversion/..., golangci-lint run all clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The retry logic is narrowly scoped to transient conditions, preserves cancellation semantics, and is covered by deterministic unit tests for the key retry and fail-fast paths.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@raelga

Copy link
Copy Markdown
Collaborator Author

/retest-required

@raelga

Copy link
Copy Markdown
Collaborator Author

ci/prow/e2e-parallel failed with 56/101 unrelated tests failing on the same RP-side error: InternalServerError: [cosmosCluster] .api.url is empty; [hypershiftHostedCluster] ReadDesire has no kube content; [servingCABundle] ServingCABundle not yet populated; [roleAssignments] role assignments not yet confirmed. This spans autoscaling, KMS rotation, workload identity, ARM64 nodepools, external auth, etc, none of which touch this PR's diff (Cincinnati control-plane-version retry logic). This is a test-environment RP outage, not a regression from this change. Requested a retest.

@raelga

Copy link
Copy Markdown
Collaborator Author

/retest

@sclarkso

Copy link
Copy Markdown
Collaborator

/test e2e-parallel

@raelga
Rael Garcia (raelga) force-pushed the rael/aroslsre-2030-cincinnati-retry branch from 982e792 to dc8fecc Compare September 7, 2026 17:16
Comment thread backend/pkg/controllers/controlplaneversion/cincinnati.go Outdated
…OSLSRE-2030)

isTransientTransportError checked net.Error before the syscall.ECONNRESET/
ECONNREFUSED/ETIMEDOUT case. Since *net.OpError implements net.Error and
its Timeout() returns false for a plain connection reset, that check
short-circuited before ever reaching the syscall case, so ECONNRESET
wasn't retried. Move the syscall and io.EOF checks ahead of the net.Error
check so those errors are retried as intended.
Copilot AI review requested due to automatic review settings September 8, 2026 07:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The retry logic is narrowly scoped, preserves fail-fast semantics for non-transient failures, and is covered by deterministic unit tests for the key behaviors.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

1 similar comment
@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

@hbhushan3

Copy link
Copy Markdown
Collaborator

/lgtm

@openshift-ci

openshift-ci Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: hbhushan3, raelga

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

The pull request process is described 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

@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

/retest-required

Remaining retests: 0 against base HEAD 80581af and 2 for PR HEAD 9935b48 in total

@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

3 similar comments
@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

@raelga

Copy link
Copy Markdown
Collaborator Author

/test e2e-parallel

@openshift-merge-bot
openshift-merge-bot Bot merged commit 56500af into Azure:main Sep 9, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants