Skip to content

fix(test-integration): honor Retry-After HTTP headers on GET requests. - #6838

Merged
openshift-merge-bot[bot] merged 2 commits into
Azure:mainfrom
bennerv:bvesel/admin-race-condition
Sep 9, 2026
Merged

fix(test-integration): honor Retry-After HTTP headers on GET requests. #6838
openshift-merge-bot[bot] merged 2 commits into
Azure:mainfrom
bennerv:bvesel/admin-race-condition

Conversation

@bennerv

Copy link
Copy Markdown
Collaborator

What

Fixes flaky integration admin tests.

Why

The breakglass admin integration test was flaky under CPU load. kubernetesApply writes to the fake object tracker synchronously, but the handler reads the session through an informer lister cache updated asynchronously by the watch goroutine. An immediate read-after-write in the next step could observe the stale (not-ready) session, so step 6 got "Session is not ready" instead of the kubeconfig. The handler only falls back to the live client on NotFound, and the fake tracker does not bump resourceVersion, so nothing bridged the gap.

Similar to our GAs, let's honor the retry-after header if the session isn't immediately ready to allow time for the lister cache to get updated by the informer.

Testing

Used the script to replicate, no longer happening after the change.

#!/bin/bash

NCPU=$(nproc); echo "saturating $NCPU cores"
for c in $(seq 1 $NCPU); do ( while :; do :; done ) & done
HOGS=$(jobs -p)

# background churn: keep re-running the mock suite to stir the scheduler
( for r in $(seq 1 60); do go test ./test-integration/admin/ -run 'TestAdminCRUD/WithMock' -count=1 >/dev/null 2>&1; done ) &
CHURN=$!

for i in $(seq 1 25); do
  if ! go test ./test-integration/admin/ -run 'TestAdminCRUD/WithMock/HCP/breakglass$' -count=4 -race >/tmp/bg_$i.log 2>&1; then
    echo "=== FAILED on iteration $i ==="
    grep -E "FAIL|Session is not ready|--- FAIL" /tmp/bg_$i.log | head -15
    break
  else
    echo "iter $i: PASS"
  fi
done

# ALWAYS clean up the busy-loops afterwards:
kill $HOGS 2>/dev/null; kill $CHURN 2>/dev/null; wait 2>/dev/null

Testing is required for feature completion and tests should be part of the pull
request along with the feature changes.

Describe the testing provided. If you did not add tests, provide a clear
justification.

Special notes for your reviewer

PR Checklist

  • PR is scoped to a single task (no mixed concerns)
  • Title follows Conventional Commits format
  • Summary explains the "Why" behind the change
  • Linked to relevant ticket/issue
  • Screenshots included (if dashboards or other UI changes)
  • Self-reviewed the diff
  • CI/CD checks are passing (ignore Tide)
  • Draft PR used for WIP (if applicable)
  • Commit history is clean (rebased/squashed)
  • Tricky code blocks are commented
  • Specific reviewers tagged
  • All comment threads resolved before merge

If E2E tests are included:

  • E2E tests follow Principles of Good E2E Test Case Design
  • If new E2E use case is covered (via a new test or new check/verifier),
    demonstrate that the test is able to detect a defect/error and fail with
    proper error message and logs which communicates nature of the problem.

Comment thread test-integration/utils/databasemutationhelpers/http_test_accessor.go Outdated

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-After retry loop has a correctness issue where a large Retry-After value can sleep past the intended overall timeout bound.

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

Pull request overview

This PR reduces flakiness in test-integration by teaching the HTTP GET step helper to honor Retry-After headers, allowing eventual-consistency paths (like informer/lister caches) time to converge before asserting the final expected response.

Changes:

  • Add a bounded retry loop to httpGet steps that re-GETs only when the server requests retry via Retry-After.
  • Extend the HTTP test accessor GET response to return both decoded body and response headers (GetResponse), and update affected tests/call sites accordingly.
File summaries
File Description
test-integration/utils/databasemutationhelpers/step_httpget.go Implements GET retry behavior driven by Retry-After, plus helper functions for matching/asserting bodies.
test-integration/utils/databasemutationhelpers/per_resource_http.go Wraps frontend SDK GET results into GetResponse (body + optional headers).
test-integration/utils/databasemutationhelpers/http_test_accessor.go Introduces GetResponse and plumbs response headers through the raw HTTP accessor.
test-integration/frontend/version_compliance_test.go Updates GET assertions to unwrap GetResponse.Body.
test-integration/frontend/cross_version_roundtrip_test.go Updates GET assertions to unwrap GetResponse.Body.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • 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 test-integration/utils/databasemutationhelpers/step_httpget.go Outdated
Comment thread test-integration/utils/databasemutationhelpers/step_httpget.go
…kglass test

The breakglass admin integration test was flaky under CPU load. kubernetesApply writes to the fake object tracker synchronously, but the breakglass kubeconfig handler reads the session through an informer lister cache that the watch goroutine updates asynchronously. An immediate read-after-write in the next step could observe the stale (not-ready) session, so step 6 got "Session is not ready" (HTTP 202) instead of the kubeconfig. This is the eventual consistency a real client already handles via the endpoint's 202 + Retry-After contract.

Make the httpGet step honor that contract: when the response does not yet match the expectation and carries a Retry-After header, re-issue the GET, waiting the server-specified delay (bounded by an overall safety timeout). Because it retries only when the response does not already match, steps that assert a not-ready/Retry-After body still pass on the first attempt - so this is safe for every httpGet with no new step type or per-step configuration. HTTPTestAccessor.Get now returns a *GetResponse (decoded body + response headers) as any; callers type-assert to read Body/Header, and the SDK-backed accessor leaves Header nil (no Retry-After, so no retry). Test-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 4, 2026 20:10
@bennerv
Ben Vesel (bennerv) force-pushed the bvesel/admin-race-condition branch from fb823f4 to 13e7ff1 Compare September 4, 2026 20:10

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 GET retry behavior cannot currently honor Retry-After on non-2xx responses because headers are dropped/ignored on error paths, which undermines the stated intent.

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

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

Comment thread test-integration/utils/databasemutationhelpers/http_test_accessor.go Outdated
Comment thread test-integration/utils/databasemutationhelpers/step_httpget.go Outdated
@bennerv

Copy link
Copy Markdown
Collaborator Author

/retest-required

Copilot AI review requested due to automatic review settings September 8, 2026 15:53

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 PR is missing a required tracking ticket link, and the updated HTTP accessor methods should drain successful response bodies before closing to avoid connection churn in integration tests.

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

Review details

Suppressed comments (3)

test-integration/utils/databasemutationhelpers/http_test_accessor.go:125

  • This method closes the response body without draining it; that prevents HTTP connection reuse and can add avoidable overhead in integration tests. Drain the body (discard) before closing.
	resp, err := a.doRequest(ctx, http.MethodPost, resourceIDString, content)
	if err != nil {
		return err
	}
	return resp.Body.Close()

test-integration/utils/databasemutationhelpers/http_test_accessor.go:133

  • This method closes the response body without draining it. To allow net/http connection reuse (and reduce connection churn in the test suite), drain the body to io.Discard before closing.
	resp, err := a.doRequest(ctx, http.MethodPatch, resourceIDString, content)
	if err != nil {
		return err
	}
	return resp.Body.Close()

test-integration/utils/databasemutationhelpers/http_test_accessor.go:141

  • This method closes the response body without draining it, which prevents HTTP keep-alive reuse and can lead to extra connection setup during integration tests. Drain the body (discard) before closing.
	resp, err := a.doRequest(ctx, http.MethodDelete, resourceIDString, nil)
	if err != nil {
		return err
	}
	return resp.Body.Close()
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +113 to +117
resp, err := a.doRequest(ctx, http.MethodPut, resourceIDString, content)
if err != nil {
return err
}
return resp.Body.Close()
Copilot AI review requested due to automatic review settings September 8, 2026 16:48
@bennerv
Ben Vesel (bennerv) force-pushed the bvesel/admin-race-condition branch from 044e5a4 to ec57484 Compare September 8, 2026 16:48

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 GET retry loop’s timeout budget does not currently bound a stalled in-flight GET (and timer handling can be tightened), which undermines the “safety bound” intent for test reliability.

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

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

Comment on lines +111 to +120
// Bounding the retries with a context rather than a manual deadline means an
// over-large Retry-After can't overshoot retryAfterTimeout: whichever of the
// two fires first wins the select below. The GET itself keeps the parent
// ctx, so a slow in-flight request isn't turned into a context error.
retryCtx, cancel := context.WithTimeout(ctx, retryAfterTimeout)
defer cancel()

for {
resp, err := accessor.Get(ctx, l.key.ResourceID)

Comment on lines +144 to +146
case <-retryCtx.Done():
timer.Stop()
// Out of budget (or the parent ctx went away). Assert on the last
…or.Get

The preceding commit needed response headers (Retry-After) on the GET path.
That was bolted on by wrapping the decoded body in a `GetResponse{Body,
Header}` struct behind an `any` return, which forced every caller into an
unchecked type assertion to recover a type the accessor already knew.

Return the raw `*http.Response` instead. doRequest still validates the status
code, consuming the body to build the error on non-2xx, but on success hands
the response back untouched. Callers read `resp.Header` directly and decode
via the new DecodeResponseBody, which reads, decodes (JSON or YAML) and
closes the body. The mutating verbs call it too and discard the value: they
do not want the content, but reading to completion drains the connection for
reuse and surfaces a malformed 2xx body as an error, both of which the old
doRequest gave every verb for free.

Also bounds the httpGet retry loop with a derived context rather than a
manual deadline. Previously each Retry-After delay had to be capped against
the remaining budget so a large server value could not sleep past
retryAfterTimeout; with a context, whichever expires first simply wins the
select. On expiry the loop falls through to the same assert as a definitive
response instead of reporting a context error, so the failure shows the
actual/expected diff that kept it retrying. The GET keeps the parent ctx so a
slow in-flight request is not converted into a context error, and time.After
becomes an explicitly stopped time.NewTimer, since a large Retry-After would
otherwise pin a timer until it fired.

There is no wait.Poll* helper that fits this loop: ConditionWithContextFunc
is func(context.Context) (done bool, err error), giving the condition no way
to dictate the next interval, and every Poll variant takes the interval as a
caller-supplied constant. Using one would mean discarding the server's
Retry-After value.

frontendHTTPTestAccessor is deleted rather than converted. It has been dead
since 5dbeb2e switched the harness to NewVersionedHTTPTestAccessor so tests
could be parameterized by API version; before this commit the only references
to it in the workspace were its own declarations. Converting it is possible —
the generated SDK is azcore-based, so policy.WithCaptureResponse could
surface the raw response — but that is plumbing written for zero callers, and
the alternative was keeping the interface at `any` to accommodate them. Note
that 468f0b0 on sudobrendan/feat-api-version-overlays removes the same file
independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bennerv
Ben Vesel (bennerv) force-pushed the bvesel/admin-race-condition branch from ec57484 to fd2dd2c Compare September 8, 2026 17:07
Copilot AI review requested due to automatic review settings September 8, 2026 17:07

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 loop can still hang on stalled GET requests (timeout doesn’t bound in-flight requests) and the loop can fail prematurely on nil/empty bodies due to ResourceInstanceEquals using require internally.

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

Review details

Suppressed comments (1)

test-integration/utils/databasemutationhelpers/step_httpget.go:119

  • The retry loop is bounded by retryCtx, but the HTTP GET itself uses the parent ctx, which has no deadline (steps use t.Context()). A stalled/hung request can therefore exceed retryAfterTimeout and hang the step/test despite the intended safety bound. Consider issuing the request with retryCtx so the overall retry budget also bounds in-flight requests.
	retryCtx, cancel := context.WithTimeout(ctx, retryAfterTimeout)
	defer cancel()

	for {
		resp, err := accessor.Get(ctx, l.key.ResourceID)
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +162 to +166
if err != nil {
return false
}
_, equals := ResourceInstanceEquals(t, l.expectedResource, body)
return equals
@geoberle

Copy link
Copy Markdown
Collaborator

/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Sep 8, 2026
@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: bennerv, geoberle

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

@bennerv

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 076bb8f and 2 for PR HEAD fd2dd2c in total

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

/retest-required

Remaining retests: 0 against base HEAD 60cecbc and 1 for PR HEAD fd2dd2c in total

@bennerv

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 c541476 and 0 for PR HEAD fd2dd2c in total

@openshift-merge-bot
openshift-merge-bot Bot merged commit c27c305 into Azure:main Sep 9, 2026
15 checks passed
@bennerv
Ben Vesel (bennerv) deleted the bvesel/admin-race-condition branch September 9, 2026 18:42
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.

3 participants