Skip to content

A75: Implement the new LB policy topology for the non-aggregate cluster - #9234

Open
mswierq wants to merge 20 commits into
grpc:masterfrom
mswierq:a75/non-aggregate-lb-policy-tree-refactoring
Open

A75: Implement the new LB policy topology for the non-aggregate cluster#9234
mswierq wants to merge 20 commits into
grpc:masterfrom
mswierq:a75/non-aggregate-lb-policy-tree-refactoring

Conversation

@mswierq

@mswierq mswierq commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Implements A75 for the non-aggregate cluster.

This change implements the new LB policy topology for the non-aggregate clusters only, the aggregate cluster uses the legacy approach. Refactoring of the new topology for the aggregate cluster case will be delivered in a follow-up PR. This change does not break non-aggregate to aggregate (and vice versa) switch.

RELEASE NOTES: none

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.55%. Comparing base (3b0f758) to head (aa09429).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
internal/xds/balancer/cdsbalancer/cdsbalancer.go 85.71% 5 Missing ⚠️
internal/xds/balancer/cdsbalancer/configbuilder.go 96.42% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9234      +/-   ##
==========================================
+ Coverage   87.52%   87.55%   +0.03%     
==========================================
  Files         430      430              
  Lines       30678    30741      +63     
==========================================
+ Hits        26850    26916      +66     
+ Misses       3827     3824       -3     
  Partials        1        1              
Files with missing lines Coverage Δ
internal/xds/balancer/cdsbalancer/configbuilder.go 95.60% <96.42%> (+0.18%) ⬆️
internal/xds/balancer/cdsbalancer/cdsbalancer.go 77.17% <85.71%> (+5.17%) ⬆️

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@easwars
easwars requested a review from Pranjali-2501 July 15, 2026 18:13
@easwars easwars added Type: Feature New features or improvements in behavior Area: xDS Includes everything xDS related, including LB policies used with xDS. labels Jul 15, 2026
@easwars easwars added this to the 1.84 Release milestone Jul 15, 2026
@easwars

easwars commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the CDS balancer to support the gRFC A75 topology for single (non-aggregate) clusters, routing them through an outlier detection policy instead of a priority policy. The changes include updating the configuration builder to construct the new balancer tree (outlier_detection -> cluster_impl -> priority -> wrr_locality -> round_robin) and updating corresponding tests. The review feedback highlights several critical areas in the new code paths where missing configuration data (such as nil cluster configs, endpoint configs, or empty priorities) could lead to nil pointer dereferences or out-of-bounds panics, and suggests adding defensive checks to ensure robustness.

Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/configbuilder.go Outdated
@mswierq
mswierq requested a review from Pranjali-2501 August 4, 2026 11:13
@easwars easwars assigned Pranjali-2501 and unassigned mswierq Aug 11, 2026
Comment thread internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go Outdated
Comment on lines +260 to +264
if isAggregate {
childCfgBytes, endpoints, err = buildAggregateClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
} else {
childCfgBytes, endpoints, err = buildLeafClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
}

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.

I think the variable topLBName can be set here only instead of setting it in another if-else block.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

after reverting gracefulswitch this actually has to stay that way, because we need to check first if the top policy type has changed due to update (e.g. update from non-aggregate to aggregate cluster)

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.

I think we can call buildAggregateClusterConfigJSON at the time of identifying the topLBName. Building the Cluster Config is not dependant on creating the child balancer of CDS.

Suggested change
if isAggregate {
childCfgBytes, endpoints, err = buildAggregateClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
} else {
childCfgBytes, endpoints, err = buildLeafClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
}
var (
topLBName string
childCfgBytes []byte
endpoints []resolver.Endpoint
err error
)
if isAggregate {
topLBName = priority.Name
childCfgBytes, endpoints, err = buildAggregateClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
} else {
topLBName = outlierdetection.Name
childCfgBytes, endpoints, err = buildLeafClusterConfigJSON(b.priorities, &b.xdsLBPolicy)
}
if err != nil {
return fmt.Errorf("failed to build child policy config: %v", err)
}

Comment on lines +155 to +157
select {
case <-exitIdleCh:
default:

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.

Why do we need this here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This might be called more than once in test cases where balancers are being reconfigured, e.g. transition from non-aggregate to aggregate cluster (TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate).

However, those channels are not used in the mentioned aggregate test case. We may consider having these exitIdleCh and closeCh channels configurable and remove this default option. WDYT?

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.

Yes, have a default case in a switch introduced non-deterministic behavior in tests leading to hard to debug flaky tests. If all we care about is whether ExitIdle was invoked or Close was invoked, we would even change these fields to a grpcsync.Event instead of a vanilla channel. The former type provides both a channel (for tests to block) and an atomic (for tests to verify state without blocking).

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.

Ping on this one.

Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder_test.go Outdated
@mswierq
mswierq requested a review from Pranjali-2501 August 12, 2026 14:54
@easwars easwars assigned Pranjali-2501 and unassigned mswierq Aug 12, 2026

@Pranjali-2501 Pranjali-2501 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.

LGTM with some minor comments.

Comment thread internal/xds/balancer/cdsbalancer/configbuilder_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/configbuilder_test.go Outdated

@easwars easwars 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.

Posting some comments that I seem to have queued up a while back.

Doing a full pass in sometime though.

Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go Outdated
Comment on lines +155 to +157
select {
case <-exitIdleCh:
default:

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.

Yes, have a default case in a switch introduced non-deterministic behavior in tests leading to hard to debug flaky tests. If all we care about is whether ExitIdle was invoked or Close was invoked, we would even change these fields to a grpcsync.Event instead of a vanilla channel. The former type provides both a channel (for tests to block) and an atomic (for tests to verify state without blocking).

Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer_test.go Outdated
Comment thread internal/xds/balancer/cdsbalancer/cdsbalancer_test.go Outdated
@mswierq
mswierq requested a review from easwars September 11, 2026 17:42
//
// Each priorityConfig corresponds to one leaf cluster retrieved from XDSConfig
// for the top-level cluster.
type priorityConfig struct {

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.

The name priorityConfig has become quite confusing, especially under the new A75 topology. Each instance of this struct actually represents the state and configuration of a single leaf cluster (EDS or LOGICAL_DNS), while the leaf cluster itself contains multiple priority levels within its EDSUpdate.Localities.

Having this type named priorityConfig causes confusion across the implementation:

  • In cdsBalancer, b.priorities has len == 1 for non-aggregate clusters.
  • In buildLeafClusterConfigJSON, we take a slice priorities []*priorityConfig and unconditionally index priorities[0] (risking a panic if empty), even though a non-aggregate cluster inherently only ever has a single leaf cluster config.

Can we rename this type to leafClusterConfig (which matches buildLeafClusterConfig and the docstring on line 48), and clean up the surrounding fields and signatures as follows:

  • Rename the type and update its docstring in configbuilder.go
  • Update buildLeafClusterConfigJSON and buildLeafClusterConfig signatures
    • Instead of taking a slice, buildLeafClusterConfigJSON should take a single *leafClusterConfig
    • And buildLeafClusterConfig parameter renamed from p *priorityConfig to leaf *leafClusterConfig
  • Update fields and helpers in cdsbalancer.go:
    • Rename priorityConfigs map[string]*priorityConfig to leafConfigs map[string]*leafClusterConfig (or leafClusters)
    • Rename priorities []*priorityConfig to leafClusters []*leafClusterConfig
    • Rename updatePriorityConfig(...) to updateLeafClusterConfig(...)
    • In updateChildConfig(), pass the single leaf cluster config cleanly
  • Update aggregate helpers in configbuilder.go:
    • buildAggregateClusterConfigJSON(leafClusters []*leafClusterConfig, ...)
    • buildAggregateClusterConfig(leafClusters []*leafClusterConfig, ...)

This makes the distinction between leaf clusters and priority levels crystal clear throughout both files and removes the unchecked slice indexing.

Comment on lines +268 to +271
if b.childLB != nil && b.childLBName != childPolicyName {
b.childLB.Close()
b.childLB = nil
}

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.

When closing and setting b.childLB = nil here, we should also reset b.childLBName = "" and b.childConfigParser = nil. Otherwise, if newChildBalancer fails below, the balancer is left with b.childLB == nil but holding stale values for childLBName and childConfigParser.

The same cleanup is needed in closeChildPolicyAndReportTF() and Close(). We could add a small helper:

func (b *cdsBalancer) closeChildPolicy() {
	if b.childLB != nil {
		b.childLB.Close()
		b.childLB = nil
		b.childLBName = ""
		b.childConfigParser = nil
	}
}

and use it in all three places to keep the three fields in sync.

// do not need to be guarded by a mutex.
cc balancer.ClientConn // ClientConn interface passed to child LB.
bOpts balancer.BuildOptions // BuildOptions passed to child LB.
childConfigParser balancer.ConfigParser // Config parser for cluster_resolver LB policy.

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.

The trailing comment on this field is stale now. Also, this field is no longer set at build time and no longer is read-only after that. So, this should be moved to the block below which contains fields that are accessed from methods implementing the balancer.Balancer interface.

@easwars easwars assigned mswierq and unassigned easwars Sep 11, 2026
Comment on lines +142 to +145
var retEndpoint resolver.Endpoint
for _, e := range endpoints {
retEndpoint.Addresses = append(retEndpoint.Addresses, e.Addresses...)
}

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.

If endpoints is empty (e.g. DNS resolution returned no addresses), this currently creates an endpoint with Addresses == nil, attaches attributes to it, and appends it to retEndpoints. As a result, retEndpoints contains a single endpoint with 0 addresses, which child balancers do not expect.

In contrast, the aggregate DNS builder (buildClusterImplConfigForDNS) explicitly checks if len(endpoints) == 0 { return pName, lbconfig, nil }.

Could we guard the endpoint construction and append with if len(endpoints) > 0 { ... } so retEndpoints remains empty when there are no DNS endpoints?

Also, please consider adding an empty_endpoints test case to TestBuildLeafClusterConfig_DNS in configbuilder_test.go to cover this scenario.

return ret, endpoints, nil
}

func buildLeafClusterConfig(p *priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*outlierdetection.LBConfig, []resolver.Endpoint, error) {

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.

While this function's signature is set up to return an error, it never returns one. Should we add a default case to the switch clusterUpdate.ClusterType and return an error?

IgnoreReresolutionRequests: true,
}
}
case xdsresource.ClusterTypeLogicalDNS:

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.

Could we preserve the explanatory comments from buildClusterImplConfigForDNS here? Specifically, explaining why we aggregate into a single logical endpoint, why an empty locality attribute is added (so locality-based policies like weighted_target continue to work), and why the locality weight is set to 1. They provide helpful context for why these attributes are needed for LOGICAL_DNS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: xDS Includes everything xDS related, including LB policies used with xDS. Status: Requires Reporter Clarification Type: Feature New features or improvements in behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants