Skip to content

fix(gatewayapi): don't skip MergeBackends precedence-index claims for policies that set nothing - #9590

Open
muwaqar-cflt wants to merge 9 commits into
envoyproxy:mainfrom
muwaqar:fix-routingtype-mergetype-9541
Open

fix(gatewayapi): don't skip MergeBackends precedence-index claims for policies that set nothing#9590
muwaqar-cflt wants to merge 9 commits into
envoyproxy:mainfrom
muwaqar:fix-routingtype-mergetype-9541

Conversation

@muwaqar-cflt

@muwaqar-cflt muwaqar-cflt commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it:

MergeBackends' precedence-resolution indexes for BackendTrafficPolicy (RoutingType, cluster-scoped settings eligibility, LoadBalancer ConsistentHash) and ClientTrafficPolicy (cluster-scoped settings eligibility) all shared the same bug: population logic skipped any accepted policy that set nothing at its target, regardless of mergeType. For a route-rule/route-targeted BTP with mergeType also unset (meaning it doesn't merge with any parent at all), this let the lookup silently fall through to a broader listener/gateway policy's settings instead of resolving to the rule's own (empty) default - or let a skipped policy's target slot be silently claimed by a younger, conflicting policy instead.

This migrates all of BTPRoutingTypeIndex, BTPClusterSettingsIndex, BTPLoadBalancerIndex, and the newly-introduced CTPClusterSettingsIndex onto a shared, minimal policyIndex[T] primitive built on the existing policyScope type (already used by SecurityPolicy's override/merge tracking), rather than patching the old ad hoc raw-map-based indexes directly. Every accepted policy now claims its precedence slot unconditionally (even when it sets nothing), preserving the existing "oldest accepted policy wins over a younger conflicting one" guarantee - dedicated tests cover this at route-rule/route/listener/gateway/ListenerSet level for each index.

policyScope gains a RouteKind tag (a 1-byte enum, to keep the struct under gocritic's hugeParam threshold) so route-rule/route scopes targeting different route kinds under the same name never collide. The BTP indexes also gained ListenerSet-attach support (Gateway/Listener/ListenerSet/ListenerSet-listener levels), extending #9419/#9425's ListenerSet-for-BTP work to this precedence layer.

Out of scope: #9619 (ClusterSettings/LoadBalancer indexes don't yet track ListenerSet-level BTP attachment the way RoutingType now does) is a related but separate gap, left for a follow-up.

Which issue(s) this PR fixes:

Fixes #9541
Fixes #9588


PR Checklist

  • Authorship & ownership: Coding agents / AI assistants are welcome, but I have reviewed every change, understand how and why it works, can explain and maintain it, and take full responsibility for this PR. I have not submitted generated output I do not understand.
  • DCO: All commits are signed off (git commit -s).
  • API agreed first: N/A - no changes under /api.
  • Required checks pass: go build ./..., go vet ./..., gofmt -l, make lint.golint, and go test ./... all pass. make gen-check shows no drift beyond this PR's own changes.
  • Tests added/updated: Unit tests added/updated in backendtrafficpolicy_test.go, clienttrafficpolicy_test.go, and a new policyindex_test.go covering the shared primitive directly (including oldest-accepted-vs-younger-conflicting-policy cases at every level), plus three new golden translator fixtures.
  • Docs: N/A - no user-facing doc change; this restores already-documented precedence semantics.
  • Release notes: Added release-notes/current/bug_fixes/9541-9588-mergebackends-precedence-index-early-exit.md.
  • Generated files committed: N/A - no API/helm/proto changes.
  • Scope & compatibility: Scoped to the four MergeBackends precedence indexes (BTPRoutingTypeIndex, BTPClusterSettingsIndex, BTPLoadBalancerIndex, CTPClusterSettingsIndex); no breaking changes.
  • Codex review: Not yet requested.
  • Copilot review: Not yet requested.

@muwaqar-cflt
muwaqar-cflt requested a review from a team as a code owner July 28, 2026 15:31
@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for cerulean-figolla-1f9435 ready!

Name Link
🔨 Latest commit 37a8203
🔍 Latest deploy log https://app.netlify.com/projects/cerulean-figolla-1f9435/deploys/6a6cbf980bb9980008eb1b68
😎 Deploy Preview https://deploy-preview-9590--cerulean-figolla-1f9435.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@muwaqar-cflt

Copy link
Copy Markdown
Contributor Author

@kkk777-7 can you review this? its ported from muwaqar#4

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.04%. Comparing base (0e77573) to head (37a8203).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/gatewayapi/policyindex.go 92.85% 2 Missing and 2 partials ⚠️
internal/gatewayapi/helpers.go 88.88% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9590      +/-   ##
==========================================
+ Coverage   76.02%   76.04%   +0.01%     
==========================================
  Files         259      260       +1     
  Lines       43275    43254      -21     
==========================================
- Hits        32900    32891       -9     
+ Misses       8183     8175       -8     
+ Partials     2192     2188       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

kkk777-7
kkk777-7 previously approved these changes Jul 29, 2026
@kkk777-7

Copy link
Copy Markdown
Member

LGTM, thanks!

@kkk777-7

Copy link
Copy Markdown
Member

/retest

Comment thread internal/gatewayapi/backendtrafficpolicy.go Outdated
Comment thread internal/gatewayapi/backendtrafficpolicy.go
Comment thread internal/gatewayapi/policyindex.go
zhaohuabing
zhaohuabing previously approved these changes Jul 30, 2026

@zhaohuabing zhaohuabing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks!

…ergeType is unset

Fixes envoyproxy#9541: a route-rule/route-targeted
BackendTrafficPolicy with both RoutingType and MergeType unset
incorrectly inherited RoutingType from its resolved gateway/listener
parent. MergeType nil means the policy doesn't merge with a parent at
all, so it must resolve to its own (nil) value instead.

Migrates BTPRoutingTypeIndex onto a new, generic policyIndex[T]
primitive that reuses the existing policyScope type (previously used
only by SecurityPolicy's override/merge tracking), rather than
patching the old ad hoc btpRoutingKey-based maps directly. Each entry
carries a single effective bool decided at write time - via
isRouteEffective's MergeType-aware rule for route-rule/route scopes,
or the caller-supplied hasValue for listener/gateway scopes - so
Lookup resolves any scope uniformly.

policyScope gains a RouteKind tag (a 1-byte enum, to keep the struct
under gocritic's hugeParam threshold) so route-rule/route scopes
targeting different route kinds under the same name never collide.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
…et fix

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
…ngs, LoadBalancer, and ClientTrafficPolicy

Applies the same "every accepted policy must claim its precedence slot,
even one that sets nothing" fix from BTPRoutingTypeIndex to
BTPClusterSettingsIndex, BTPLoadBalancerIndex, and the newly-migrated
CTPClusterSettingsIndex, closing the same class of bug for envoyproxy#9588. All
three indexes are now built on the shared policyIndex[T] primitive
instead of ad-hoc raw maps, and the BTP indexes gained ListenerSet
support.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
@muwaqar-cflt muwaqar-cflt changed the title fix(gatewayapi): don't inherit RoutingType when MergeType is unset fix(gatewayapi): don't skip MergeBackends precedence-index claims for policies that set nothing Jul 30, 2026
@jukie

jukie commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@codex review

….go and its test

The test file had grown its own copy of route.go's listener-scope-checking
loop just to keep compiling after CTPClusterSettingsIndex became a plain
policyIndex alias, so the test wasn't actually exercising production
code and could silently drift from it. Extract the loop into a package
function both call.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e4ccd881f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/gatewayapi/backendtrafficpolicy.go Outdated
…onally

The LoadBalancer switch only ran under `if hasLoadBalancer`, so an
accepted Gateway-wide BTP that left LoadBalancer unset skipped claiming
its slot entirely - letting a younger conflicting BTP that does set
ConsistentHash win it. Same "must claim slot unconditionally" fix
already applied to RoutingType and ClusterSettings.

Found via automated PR review on envoyproxy#9590.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
@muwaqar-cflt

Copy link
Copy Markdown
Contributor Author

A note on scope: this PR originally targeted BTPRoutingTypeIndex only, so it could be cherry-picked cleanly to older release branches (e.g. 1.8.x). Unfortunately, MergeBackends (#9477) merged to main in the meantime, and it isn't possible to keep this fix scoped to RoutingType alone anymore - a bunch of interrelated code (BTPClusterSettingsIndex, BTPLoadBalancerIndex, CTPClusterSettingsIndex) landed alongside it that shares the exact same bug class, and fixing forward on top of main means addressing all of them together rather than re-introducing the old ad hoc raw-map indexes just to keep the diff narrow.

So the scope here is intentionally wider than the original description, now covering #9541 and #9588. For 1.8.4 (which doesn't have MergeBackends' ClusterSettings/LoadBalancer/CTP indexes at all), a separate, isolated fix scoped to RoutingType only will be needed - this PR won't cherry-pick cleanly there.

…aliasing

Go won't allow new methods on a type alias to an instantiated generic
(the reason these indexes were reduced to plain policyIndex[T]
aliases), but a struct that anonymously embeds *policyIndex[T] is a
distinct named type, so it can have its own methods. This restores
BTPRoutingTypeIndex.LookupBTPRoutingType/LookupGatewayBTRoutingType,
BTPClusterSettingsIndex.HasRouteLevelClusterSettings,
BTPLoadBalancerIndex.IsConsistentHash, and
CTPClusterSettingsIndex.HasListenerLevelClusterSettings - matching
main's PR #4 design - while keeping call sites free of any .index
field indirection: because the embed is anonymous, methods forward via
promotion (idx.Lookup(...), not idx.index.Lookup(...)). Each dedicated
method checks for a nil receiver before touching the embedded field,
so nil-safety doesn't regress.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
…omments

- Port TestCTPClusterSettingsIndex's 2 missing oldest-accepted-blocks-
  younger regression cases (Gateway-listener and ListenerSet-wide
  level), matching the same coverage already ported for BTP indexes.
- Port the missing mergebackends-http-demerge-route-replaces-gateway-
  cluster-settings golden fixture pair, verified byte-identical to
  PR #4's own validated output (aside from the mergeBackends schema
  drift already fixed in the other 3 ported fixtures).
- Strip every test-case comment added by this PR (per review feedback);
  comments already present before this PR are left untouched.
- Merge TestPolicyIndexLookupListenerSet into TestPolicyIndexLookup for
  consistency with TestPolicyIndexLookupExact's single-table style, and
  settle on a consistent useRuleName/useListener/useListenerSet naming
  convention for the three optional Lookup arguments.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
@zhaohuabing
zhaohuabing requested review from a team, kkk777-7 and zhaohuabing July 30, 2026 22:14
name: ""
prefix: /a
traffic:
faultInjection:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice! Thanks for the fix!

@zhaohuabing

Copy link
Copy Markdown
Member

/retest

Comment thread internal/gatewayapi/backendtrafficpolicy.go Outdated
… via hasRouteLevelClusterSettings

The route/rule-level BackendTrafficPolicy conflict this TODO described
is already excluded from cluster merging by hasRouteLevelClusterSettings's
pinned-slot tracking, so it never reaches x.BackendClusters here.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
@zhaohuabing
zhaohuabing requested a review from a team July 31, 2026 15:00
…dence fix

MergeBackends itself hasn't shipped in a release yet, so a separate
bug-fix note in the same release cycle as its own new-feature note is
redundant and confusing.

Signed-off-by: Muhammad Waqar <mwaqar@confluent.io>
if idx == nil {
return false
}
value, pinned := idx.Lookup(routeKind, routeNN, gatewayNN, listenerName, nil, routeRuleName)

@zhaohuabing zhaohuabing Jul 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit, for clarity and maintainability, I'd prefer longer but more descriptive names here.

Suggested change
value, pinned := idx.Lookup(routeKind, routeNN, gatewayNN, listenerName, nil, routeRuleName)
hasExplicitClusterSettings, replacesParent := idx.lookup(routeKind, routeNN, gatewayNN, listenerName, nil, routeRuleName)

listenerName *gwapiv1.SectionName,
listenerSetNN *types.NamespacedName,
routeRuleName *gwapiv1.SectionName,
) (value T, pinned bool) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer a more descriptive name here:

Suggested change
) (value T, pinned bool) {
) (value T, replacesParent bool) {

if idx == nil {
return false
}
value, pinned := idx.Lookup(routeKind, routeNN, gatewayNN, listenerName, nil, routeRuleName)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LookupresolveEntryparentLevels eventually checks gatewayScope(gatewayNN). This is currently harmless because BuildBTPIndexes does not add gateway-scoped entries to this index. However, this constraint is not apparent from the Lookup path itself and could confuse future readers. For maintainability, could we make it explicit in the lookup logic?

The below fix is suggested by Claude code, for reference only:

type scopeDepth uint8

const (
      throughGatewayScope scopeDepth = iota
      belowGatewayScope
)

func newPolicyIndex[T comparable](depth scopeDepth) *policyIndex[T] {
	return &policyIndex[T]{entries: make(map[policyScope]policyIndexEntry[T]), depth: depth}
}

func (idx *policyIndex[T]) parentLevels(gatewayNN types.NamespacedName, listenerName *gwapiv1.SectionName, listenerSetNN *types.NamespacedName) T {
	var zero T
	if idx == nil {
		return zero
	}

	if listenerSetNN != nil {
		if listenerName != nil {
			if value, found := idx.LookupExact(listenerSetListenerScope(*listenerSetNN, *listenerName)); found {
				return value
			}
		}
		if value, found := idx.LookupExact(listenerSetScope(*listenerSetNN)); found {
			return value
		}
	} else if listenerName != nil {
		if value, found := idx.LookupExact(gatewayListenerScope(gatewayNN, *listenerName)); found {
			return value
		}
	}

	if idx.depth == belowGatewayScope {
		return zero
	}

	value, _ := idx.LookupExact(gatewayScope(gatewayNN))
	return value
}

parentLevels returns before its final gateway step when depth is belowGatewayScox is constructed with it, BTPRoutingTypeIndex keeps the full walk.

}

// setListenerSetLevel is setGatewayLevel's counterpart for a ListenerSet.
func (idx *policyIndex[T]) setListenerSetLevel(ls types.NamespacedName, value T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we also have hasValue here to determine whether we should fall back to the Gateway policy?

Comment on lines +114 to +120
// HasRouteLevelClusterSettings reports whether a route-rule, route, or listener-level
// BackendTrafficPolicy contributes backend-cluster-scoped settings for the given target, or
// targets it with MergeType unset. A gateway-level setting isn't checked: it applies uniformly to
// every route sharing a merged cluster, so it can't cause a divergence. ListenerSet-level BTP
// attachment isn't tracked here yet (envoyproxy/gateway#9619), so listenerSetNN is always resolved
// as unset.
func (idx *BTPClusterSettingsIndex) HasRouteLevelClusterSettings(

@zhaohuabing zhaohuabing Aug 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The name HasRouteLevleCLusterSettings is not percise, it covers two cases:

  • this rule's backendRefs have explicit Cluster Settings below Gateway, including routeRule, route, and listener
  • this rule's backendRefs don't have explicit Cluster Settings below Gateway,but replace Gateway-level cluser settings with nil
Suggested change
// HasRouteLevelClusterSettings reports whether a route-rule, route, or listener-level
// BackendTrafficPolicy contributes backend-cluster-scoped settings for the given target, or
// targets it with MergeType unset. A gateway-level setting isn't checked: it applies uniformly to
// every route sharing a merged cluster, so it can't cause a divergence. ListenerSet-level BTP
// attachment isn't tracked here yet (envoyproxy/gateway#9619), so listenerSetNN is always resolved
// as unset.
func (idx *BTPClusterSettingsIndex) HasRouteLevelClusterSettings(
// ClusterSettingsDefinedBelowGateway reports whether the backend-cluster-scoped settings applying
// to the given target are defined below Gateway scope: either a route-rule, route, or
// listener-level BackendTrafficPolicy contributes them, or a route-rule/route-level one targets it
// with MergeType unset, which replaces the Gateway's settings instead of merging with them - even
// when that policy sets none of its own, leaving the target with empty settings rather than the
// Gateway's. A gateway-level setting is never consulted - the index resolves belowGatewayScope, so
// the walk stops short of it: a Gateway-wide setting applies uniformly to every route sharing a
// merged cluster, so it can never be the reason two of them diverge. ListenerSet-level BTP
// attachment isn't tracked here yet (envoyproxy/gateway#9619), so listenerSetNN is always resolved
// as unset.
func (idx *BTPClusterSettingsIndex) ClusterSettingsDefinedBelowGateway(

Comment on lines +530 to 534
// hasListenerLevelClusterSettings reports whether any of the route rule's attached listeners
// (parentRef.listeners - the route's actual resolved attachment(s) under gatewayCtx's gateway) has
// a ClientTrafficPolicy-sourced HTTP1 override, checking each listener against its own owner (the
// Gateway, or the ListenerSet it came from).
func (t *Translator) hasListenerLevelClusterSettings(gatewayCtx *GatewayContext, parentRef *RouteParentContext) bool {

This comment was marked as outdated.

}
// Delegate to the remaining multi-backendRef checks.
return t.weightedRuleBackendsMustBeInOneCluster(routeCtx, backendRefs, sessionPersistent, gatewayCtx)
}

@zhaohuabing zhaohuabing Aug 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Having hasRouteLevelClusterSettings(a better name should be clusterSettingsDefinedBelowGateway) and hasListenerLevelClusterSettings called side by side reads oddly now, since "hasRouteLevelClusterSettings" already includes listener scope — the real difference is BTP vs CTP, not the scope.

Suggest folding the CTP lookup in hasRouteLevelClusterSettings, rename it to clusterSettingsDefinedBelowGateway, and dropping the second wrapper:

// clusterSettingsDefinedBelowGateway reports whether the cluster-scoped settings applying to this
// rule are defined below Gateway scope, in any of three ways:
//
//   - explicit setting: a BackendTrafficPolicy at route-rule/route/listener scope sets a
//     cluster-scoped field.
//   - empty replacement: a BackendTrafficPolicy at route-rule/route scope sets no cluster-scoped
//     field at all, but leaves MergeType unset, so it replaces the Gateway's settings rather than
//     merging with them - leaving the rule with its own empty set.
//   - client traffic: a ClientTrafficPolicy at listener scope sets an HTTP1 override.
//
// All three make cluster deduplication unsafe, though not for the same reason: the BTP settings
// would wrongly apply to the other routes sharing the cluster, while the CTP ones would be lost
// entirely, since a merged BackendCluster carries no HTTP1 settings.
//
// The CTP case checks parentRef.listeners - the route's actual resolved attachment(s) under
// gatewayCtx's gateway - against each listener's own owner (the Gateway, or the ListenerSet it
// came from).
func (t *Translator) clusterSettingsDefinedBelowGateway(
	gatewayCtx *GatewayContext,
	routeCtx RouteContext,
	parentRef *RouteParentContext,
	routeRuleName *gwapiv1.SectionName,
) bool {
	if gatewayCtx == nil {
		return false
	}
	gatewayNN := types.NamespacedName{Namespace: gatewayCtx.GetNamespace(), Name: gatewayCtx.GetName()}
	if t.BTPClusterSettingsIndex.ClusterSettingsDefinedBelowGateway(
		routeCtx.GetRouteType(),
		types.NamespacedName{Namespace: routeCtx.GetNamespace(), Name: routeCtx.GetName()},
		gatewayNN,
		parentRef.SectionName,
		routeRuleName,
	) {
		return true
	}
	return t.CTPClusterSettingsIndex.HasListenerLevelClusterSettings(gatewayNN, parentRef.listeners)
}

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.

Keep replacing route policies out of shared clusters BackendTrafficPolicy RoutingType ignores MergeType, always inherits from broader-scoped policies

6 participants