test(e2e): retry transient network errors on idempotent requests#9468
test(e2e): retry transient network errors on idempotent requests#9468Aias00 wants to merge 4 commits into
Conversation
✅ Deploy Preview for cerulean-figolla-1f9435 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
This PR hardens the e2e Kubernetes client used by Envoy Gateway test suites by adding a rest.Config transport wrapper that retries transient, connection-level failures for idempotent, replayable requests (notably teardown DELETEs), addressing intermittent conformance cleanup flakes.
Changes:
- Add a retrying
http.RoundTripper(retryTransport) and wire it into the shared e2eNewClientpath viarest.Configtransport wrapping. - Implement conservative retry rules (GET/HEAD/DELETE only; only replayable bodies; transient network errors only; no HTTP status retries).
- Add focused unit tests covering retry behavior, replaying DELETE bodies via
GetBody, attempt exhaustion, and context-cancel behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
test/utils/kubernetes/client.go |
Installs a retrying transport wrapper and implements the retry/backoff + transient-error detection logic. |
test/utils/kubernetes/client_test.go |
Adds unit tests validating retry behavior, body replay semantics, and non-retryable scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9468 +/- ##
==========================================
+ Coverage 75.41% 75.43% +0.02%
==========================================
Files 252 252
Lines 41660 41641 -19
==========================================
- Hits 31417 31412 -5
+ Misses 8113 8095 -18
- Partials 2130 2134 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The TestEGUpgrade teardown intermittently fails with a transient
connection-level error when the gateway-api conformance suite's cleanup
deletes the upgrade GatewayClass:
Delete ".../gatewayclasses/upgrade": EOF
This is a test-infra flake, not a regression in the code under test: it
shows up on unrelated PRs, and the failing DELETE runs in the suite's
registered t.Cleanup after the test body already succeeded.
Root cause: client-go's rest.Request only retries GET on transient
errors (IsProbableEOF/IsConnectionReset/IsHTTP2ConnectionLost); DELETE
and other write verbs surface the error immediately. So when a DELETE
reaches the apiserver but the response is lost (EOF / connection reset /
HTTP/2 GOAWAY / timeout), teardown fails even though the resource was
already deleted.
Fix: install a retrying http.RoundTripper via rest.Config.WrapTransport in
test/utils/kubernetes NewClient. WrapTransport is the only hook inherited
by the conformance suite's per-test client rebuild (setClientsetForTest
calls client.New(suite.RestConfig, ...)), so a client.Client wrapper would
be bypassed while the transport wrapper is not.
Retry policy (conservative):
- Methods: GET, HEAD, DELETE only. Never POST/PATCH/PUT.
- Body: only retry when replayable (nil / http.NoBody / GetBody != nil).
client-go builds DELETE bodies from bytes.NewReader, and the stdlib
auto-populates GetBody for *bytes.Reader, so DeleteOptions bodies are
replayable. http.NoBody must be checked explicitly because the stdlib
substitutes it for a zero-length body instead of nil.
- Before each retry after the first: re-derive the body via GetBody and
assign to a Clone (Clone copies GetBody but does not reset Body).
- Errors retried: EOF, ErrUnexpectedEOF, connection reset, HTTP/2
GOAWAY/connection-lost, net.Error timeouts.
- HTTP status codes are NOT retried (5xx/429/404 returned as-is), so a
404 from a retried DELETE flows through to MustApplyWithCleanup's
existing IsNotFound -> success handling.
- Close any partial resp.Body before retrying; honor context cancellation
in the backoff select.
- Compose with any pre-existing cfg.WrapTransport via transport.Wrappers.
Adds unit tests covering: EOF retry on GET, DELETE-with-body replay via
GetBody, non-retry of POST/non-replayable bodies, non-retry of HTTP
status codes (incl. 404 pass-through), attempt exhaustion, context
cancellation, and timeout (net.Error) retry.
Fixes envoyproxy#9467
Signed-off-by: liuhy <liuhongyu@apache.org>
17bf8e4 to
d9fa833
Compare
Signed-off-by: liuhy <liuhongyu@apache.org>
What type of PR is this?
/kind flake
/area testing
What this PR does
Fixes an intermittent
TestEGUpgradeteardown flake where the gateway-api conformance suite's cleanup fails to delete theupgradeGatewayClass with a transient connection error:This is test-infra flake (shows up on unrelated PRs, e.g. #9460), not a regression in the code under test.
Root cause
client-go's
rest.Requestonly retries GET on transient connection errors (IsProbableEOF/IsConnectionReset/IsHTTP2ConnectionLost); DELETE and other write verbs surface the error immediately. So when a teardown DELETE reaches the apiserver but the response is lost (EOF / connection reset / HTTP/2 GOAWAY / timeout), the test fails — even though the resource was already deleted.The teardown DELETE goes through
suite.Client, which the conformance suite rebuilds per test viaclient.New(suite.RestConfig, ...)insetClientsetForTest. So aclient.Client-level wrapper would be bypassed; the only hook inherited by those rebuilt clients isrest.Config.WrapTransport.Fix
Install a retrying
http.RoundTripperviarest.Config.WrapTransportintest/utils/kubernetes'sNewClient(composed with any pre-existing wrapper viatransport.Wrappers).Retry policy (conservative):
req.Body == nil,req.Body == http.NoBody, orreq.GetBody != nil. (client-go builds DELETE bodies frombytes.NewReader; the stdlib auto-populatesGetBodyfor*bytes.Reader, soDeleteOptionsbodies are replayable.http.NoBodymust be checked explicitly because the stdlib substitutes it for a zero-length body instead ofnil.)req.GetBody()and assign to areq.Clone(...)(Clone copiesGetBodybut does not resetBody).io.EOF,io.ErrUnexpectedEOF, connection reset, HTTP/2 GOAWAY/connection-lost,net.Errortimeouts.MustApplyWithCleanup's existingIsNotFound→ success handling.resp.Bodybefore retrying; honor context cancellation in the backoffselect.Tests
Adds
test/utils/kubernetes/client_test.gocovering:GetBody, 404 pass-through (the exactgatewayclasses/upgradeflake scenario)GetBody) not retriednet.Errortimeout retriedBlast radius:
NewClientis the shared entrypoint for all e2e suites (e2e,upgrade,multiple_gc,merge_gateways,benchmark,resilience); all compile clean.Related
Fixes #9467
Historical
TestEGUpgradeflake issues (different root causes): #6009, #3311, #3262, #2937🤖 Generated with Claude Code