A75: Implement the new LB policy topology for the non-aggregate cluster - #9234
A75: Implement the new LB policy topology for the non-aggregate cluster#9234mswierq wants to merge 20 commits into
Conversation
Codecov Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
|
/gemini review |
There was a problem hiding this comment.
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.
This reverts commit fbed93a.
…b-policy-tree-refactoring
| if isAggregate { | ||
| childCfgBytes, endpoints, err = buildAggregateClusterConfigJSON(b.priorities, &b.xdsLBPolicy) | ||
| } else { | ||
| childCfgBytes, endpoints, err = buildLeafClusterConfigJSON(b.priorities, &b.xdsLBPolicy) | ||
| } |
There was a problem hiding this comment.
I think the variable topLBName can be set here only instead of setting it in another if-else block.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
| 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) | |
| } |
| select { | ||
| case <-exitIdleCh: | ||
| default: |
There was a problem hiding this comment.
Why do we need this here?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
…b-policy-tree-refactoring
This reverts commit 51e986e.
Pranjali-2501
left a comment
There was a problem hiding this comment.
LGTM with some minor comments.
…b-policy-tree-refactoring
easwars
left a comment
There was a problem hiding this comment.
Posting some comments that I seem to have queued up a while back.
Doing a full pass in sometime though.
| select { | ||
| case <-exitIdleCh: | ||
| default: |
There was a problem hiding this comment.
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).
| // | ||
| // Each priorityConfig corresponds to one leaf cluster retrieved from XDSConfig | ||
| // for the top-level cluster. | ||
| type priorityConfig struct { |
There was a problem hiding this comment.
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.prioritieshaslen == 1for non-aggregate clusters. - In
buildLeafClusterConfigJSON, we take a slice priorities[]*priorityConfigand unconditionally indexpriorities[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
buildLeafClusterConfigJSONandbuildLeafClusterConfigsignatures- Instead of taking a slice,
buildLeafClusterConfigJSONshould take a single*leafClusterConfig - And
buildLeafClusterConfigparameter renamed fromp *priorityConfigtoleaf *leafClusterConfig
- Instead of taking a slice,
- Update fields and helpers in cdsbalancer.go:
- Rename
priorityConfigs map[string]*priorityConfigtoleafConfigs map[string]*leafClusterConfig(or leafClusters) - Rename
priorities []*priorityConfigtoleafClusters []*leafClusterConfig - Rename
updatePriorityConfig(...)toupdateLeafClusterConfig(...) - In
updateChildConfig(), pass the single leaf cluster config cleanly
- Rename
- 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.
| if b.childLB != nil && b.childLBName != childPolicyName { | ||
| b.childLB.Close() | ||
| b.childLB = nil | ||
| } |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
| var retEndpoint resolver.Endpoint | ||
| for _, e := range endpoints { | ||
| retEndpoint.Addresses = append(retEndpoint.Addresses, e.Addresses...) | ||
| } |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
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