Skip to content

fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period#14380

Merged
davidjumani merged 4 commits into
kgateway-dev:mainfrom
chandler-solo:chandler/14184revert13868andsleep2
Jul 10, 2026
Merged

fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period#14380
davidjumani merged 4 commits into
kgateway-dev:mainfrom
chandler-solo:chandler/14184revert13868andsleep2

Conversation

@chandler-solo

@chandler-solo chandler-solo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Greatly helps #14184, with an explicit trade-off: it reintroduces the race that #13868 closed (a chance of brief Envoy 5xx with NC response flags around controller restarts), but shrinks that window with a configurable first-connect grace period — trading #13868's unbounded failure mode for a bounded, self-healing one.

The problem

#13868 gated per-client xDS publication behind whole-snapshot readiness checks: if any route-referenced cluster was missing from the per-client CDS map, or any referenced EDS cluster lacked a ClusterLoadAssignment, the client's entire snapshot was withheld. When the gate stays unsatisfiable — ExternalName references, per-client derivation backlog under large unique-client fan-out — this produces the two failure modes in #14184: warm proxies stranded indefinitely on stale endpoints (upstream connect failures to deleted pods), and first-time proxies starved of any config until they fail their startup probe and crashloop. Because the gate was level-triggered and unconditional, no restart or rollout could clear it.

The fix

This PR removes the gates and replaces them with a time-based guard at the other end of the pipe: when a new xDS stream sends its first DiscoveryRequest, the control plane registers the client (kicking off per-client translation) and then sleeps briefly — default 1s, tunable via KGW_XDS_FIRST_CONNECT_DELAY, 0 or negative disables — before returning from the OnStreamRequest callback.

Why that placement works (verified against the pinned go-control-plane, pkg/server/sotw/v3/ads.go): the server invokes OnStreamRequest before creating the stream's first watch/subscription, so delaying the callback's return delays the first response — and each stream's requests are processed on that stream's own goroutine, so the sleep delays only this client. The sleep runs after the registration lock is released and fires at most once per stream (guarded by a new-stream flag from client registration). The delay value lives in an atomic so the test override can't race live stream goroutines. By the time the first watch opens, per-client translation has had a head start, so the first snapshot the client observes is almost always fully converged.

Notes on scope of the delay: it applies to every new stream, including a warm Envoy reconnecting after a controller restart — that proxy keeps serving its existing config during the delay and merely receives its first post-reconnect update up to one grace period later. Other clients and translation are unaffected.

The trade-off, quantified

This restores the original (pre-#13868) reconnect race in the tail case where per-client translation takes longer than the grace period. It manifests as a brief, self-healing burst of NC/500s rather than the gate's indefinite starvation. Field data from an affected user (5,000 req/s, ~1,500 failed requests during a controller restart) puts the pre-gate race window around 300ms; the 1s default covers that with margin. Environments with much larger unique-client fan-out × backend counts can raise the delay; the fan-out itself is being addressed separately (#14343 base+overlay restructuring; DISABLE_POD_LOCALITY_XDS as an immediate lever).

Commit structure (please review commit-by-commit)

Four commits, ordered so the mechanical reverts don't obscure the reviewable delta. Every commit compiles and passes its tests on its own (the reverts leave no dangling references — the BlackholeClusterName constant-ification in the third commit replaces string literals that exist at the second).

  1. Revert "proxy-syncer bug fix, but for a bug that never actually happens (proxy-syncer bug fix, but for a bug that never actually happens #13958)" — pure git revert. proxy-syncer bug fix, but for a bug that never actually happens #13958's empty-cluster substitution existed only to serve the gates; without them it would publish a near-empty state-of-the-world CDS to a new client. Reverted first since it was built on top of fix: remedies a reconnect-time xDS race #13868.
  2. Revert "fix: remedies a reconnect-time xDS race (fix: remedies a reconnect-time xDS race #13868)" — pure git revert of the readiness gates, the ReferencedClusters proto-walk machinery, and their tests.
  3. fix: delay first xDS response per new stream instead of gating snapshots (+402/−26 across 9 files) — the core change:
    • the first-connect delay in pkg/krtcollections/uniqueclients.go: atomic-backed and env-tunable, slept once per new stream (guarded by a new-stream flag from client registration) after registration kicks off per-client translation, outside all locks, with a test shim (export_test.go) and delay-zeroing in TestUniqueClients;
    • re-adds wellknown.BlackholeClusterName and constant-ifies its uses (kept from fix: remedies a reconnect-time xDS race #13868 as standalone cleanup);
    • re-adds three regression tests pinning the anti-starvation properties (errored clusters fail closed without blocking publication; unresolvable BackendRefs at startup and at runtime never withhold the snapshot);
    • updates the defer-guard comment in perclient.go and the no-op Delete branch comment in proxy_syncer.go to record the fix: remedies a reconnect-time xDS race #13868 -> v2.3.0 upstream connect failures bug #14184 history (including the pre-existing SnapshotCache leak for departed clients) instead of restoring the 2024-era comment text.
  4. test(e2e): pin anti-starvation properties of per-client xDS publication (+428/−20 across 10 files) — the test strategy for the delay:
    • makes the delay's environment read lazy (first use instead of package init) so test binaries can configure it from TestMain;
    • a focused contract test (TestFirstConnectDelayGatesFirstRequestPerStream) pinning the delay's five properties: the first request of a new stream absorbs the grace period; registration (which kicks off per-client translation) precedes the sleep; follow-up requests on the same stream do not sleep; a new stream sleeps again; zero disables. This is where the delay's behavior is verified — not the slow live-ADS setup suite;
    • pkg/kgateway/setup tests now run with the delay zeroed via TestMain (unless the environment overrides it): the delay is a mitigation, not a correctness requirement, so those eventually-consistent assertions must hold against the raw reconnect race the delay merely narrows — running at zero both surfaces bugs the grace period would mask and cuts the suite from ~226s to ~86s;
    • a new e2e suite XdsStarvation (test/e2e/features/xds_starvation, registered and assigned to cluster-seven): the end-to-end regression pin for the starvation family, built on the credible absence shape (an ExternalName Service, which never produces EndpointSlices). With that never-ready reference in the config, the valid route still serves, a gateway rollout's fresh pods become Ready instead of crash-looping, and a route created after a controller restart still reaches the reconnected proxies. Each of these deadlocked under the reverted gates;
    • rides along with an unrelated one-line Makefile fix found while running the suite locally: kind-create's cluster-existence guard used grep $(CLUSTER_NAME), which false-matches any cluster whose name merely contains the target (e.g. kind-foo matches kind), silently skipping creation and letting subsequent kubectl steps run against whatever cluster owns the current context. Now grep -x.

Change Type

/kind fix

Changelog

Fix a bug where per-client xDS snapshots could be withheld indefinitely (reverts #13868/#13958), stranding running gateway pods on stale endpoints and starving newly created pods of any configuration until they crashloop. The control plane now publishes snapshots unconditionally and instead delays each newly connected client's first xDS response by a short grace period (default 1s, configurable via KGW_XDS_FIRST_CONNECT_DELAY; 0 disables) so per-client translation can converge before the first snapshot is served. Trade-off: in environments where per-client translation exceeds the grace period, a controller restart can once again cause a brief, self-healing burst of upstream-connect (NC) errors on reconnecting proxies; raise KGW_XDS_FIRST_CONNECT_DELAY if you observe this.

Additional Notes

@gateway-bot gateway-bot added kind/fix Categorizes issue or PR as related to a bug. release-note labels Jul 10, 2026
@chandler-solo
chandler-solo force-pushed the chandler/14184revert13868andsleep2 branch from 525170c to 96091fe Compare July 10, 2026 15:07
@chandler-solo chandler-solo added the work in progress Indicates that a PR should not merge because it is a work in progress label Jul 10, 2026
@chandler-solo chandler-solo changed the title fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period [WIP] fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period Jul 10, 2026
@chandler-solo
chandler-solo marked this pull request as ready for review July 10, 2026 15:14
Copilot AI review requested due to automatic review settings July 10, 2026 15:14

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.

Pull request overview

This PR mitigates per-client xDS snapshot starvation by reverting the whole-snapshot readiness gates (from #13868/#13958) and instead introducing a per-stream “first-connect” grace period to give per-client translation time to converge before the first response is served. It also adds targeted unit and e2e coverage to pin the anti-starvation behavior across rollouts and controller restarts.

Changes:

  • Add a configurable per-xDS-stream first-connect delay (KGW_XDS_FIRST_CONNECT_DELAY) implemented in the uniquely-connected-clients callbacks.
  • Remove the per-client “referenced clusters / referenced endpoints” readiness gates and their proto-walk machinery, keeping only the minimal “inputs not ready yet” deferral.
  • Add regression tests (unit + e2e) for the anti-starvation properties and wire the new e2e suite into CI.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/e2e/tests/kgateway_tests.go Registers the new XdsStarvation e2e suite.
test/e2e/features/xds_starvation/testdata/setup.yaml Adds Gateway/Routes including an ExternalName backend to model a never-ready reference.
test/e2e/features/xds_starvation/testdata/postrestart-route.yaml Adds a post-restart HTTPRoute to verify warm clients still receive updates.
test/e2e/features/xds_starvation/suite.go Implements e2e assertions covering serving, gateway rollout, and controller restart behavior under a never-ready reference.
pkg/krtcollections/uniqueclients.go Implements atomic-backed, env-tunable first-connect delay applied once per new stream.
pkg/krtcollections/uniqueclients_test.go Disables first-connect delay for existing unit test suite that creates many streams.
pkg/krtcollections/uniqueclients_delay_test.go Adds a focused unit test pinning per-stream delay semantics.
pkg/krtcollections/export_test.go Exposes a test-only delay override helper for race-safe test control.
pkg/kgateway/setup/main_test.go Sets default KGW_XDS_FIRST_CONNECT_DELAY=0 for live ADS server tests unless explicitly overridden.
pkg/kgateway/proxy_syncer/proxy_syncer.go Removes referenced-cluster tracking and updates comments documenting deferred snapshot behavior.
pkg/kgateway/proxy_syncer/perclient.go Removes whole-snapshot readiness gates and updates defer rationale/comments.
pkg/kgateway/proxy_syncer/perclient_test.go Removes gate-specific tests and retains/adds regression tests ensuring snapshots still publish under certain failure modes.
Makefile Tightens KinD cluster existence check matching.
.github/workflows/e2e.yaml Adds XdsStarvation to the e2e run matrix.

Comment thread Makefile
@chandler-solo
chandler-solo force-pushed the chandler/14184revert13868andsleep2 branch from 83aac5b to d1cba91 Compare July 10, 2026 16:00
@chandler-solo chandler-solo changed the title [WIP] fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period fix: revert per-client xDS readiness gates (#13868, #13958) in favor of a first-connect grace period Jul 10, 2026
@chandler-solo chandler-solo removed the work in progress Indicates that a PR should not merge because it is a work in progress label Jul 10, 2026
@chandler-solo
chandler-solo force-pushed the chandler/14184revert13868andsleep2 branch from d1cba91 to 88cb65e Compare July 10, 2026 16:08
Now that we have done pure reverts of 13958 and 13868, a few changes
held over, an updated comment, and sleeping upon new UCC connection to
mitigate but not perfectly solve kgateway-dev#14184

Signed-off-by: David L. Chandler <[email protected]>
And a Makefile fix for many local kind clusters.

Signed-off-by: David L. Chandler <[email protected]>
@chandler-solo
chandler-solo force-pushed the chandler/14184revert13868andsleep2 branch from 88cb65e to a326f78 Compare July 10, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/fix Categorizes issue or PR as related to a bug. release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants