Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
533dafb
A75: Implement the new LB policy topology for the non-aggregate clust…
mswierq Jul 15, 2026
5516779
Merge branch 'master' into a75/non-aggregate-lb-policy-tree-refactoring
mswierq Jul 17, 2026
9b3c703
A75: Add missing test cases
mswierq Jul 17, 2026
fbed93a
Address Gemini review - fix nil checks
mswierq Jul 20, 2026
c1ab2d9
Revert "Address Gemini review - fix nil checks"
mswierq Aug 3, 2026
785be23
Fix comments and legacy code cleanup
mswierq Aug 4, 2026
51e986e
Use gracefulswitch.Balancer
mswierq Aug 4, 2026
e7c0475
Fix functions and variables names
mswierq Aug 4, 2026
d6a83ca
Do not create clusterImpl for each priority
mswierq Aug 4, 2026
31d6b4c
Merge remote-tracking branch 'origin/master' into a75/non-aggregate-l…
mswierq Aug 4, 2026
53b4209
Merge remote-tracking branch 'origin/master' into a75/non-aggregate-l…
mswierq Aug 12, 2026
9f43e37
Address feedback
mswierq Aug 12, 2026
f2fc6b5
Revert "Use gracefulswitch.Balancer"
mswierq Aug 13, 2026
6c54ef5
Merge remote-tracking branch 'origin/master' into a75/non-aggregate-l…
mswierq Aug 28, 2026
ce1c7f0
Address feedback
mswierq Aug 28, 2026
a0841ec
Address feedback: nits and new names
mswierq Sep 11, 2026
c45c12b
Merge remote-tracking branch 'origin/master' into a75/non-aggregate-l…
mswierq Sep 11, 2026
506f940
Remove unused serviceConifg
mswierq Sep 11, 2026
5657854
Address feedback: consume lb config in a detereministic way
mswierq Sep 11, 2026
aa09429
Address feedback: use grpcsync.Event
mswierq Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 25 additions & 63 deletions internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ func verifyDNSResolution(ctx context.Context, t *testing.T, dnsTargetCh chan res

// Tests the case where the cluster resource requested is a leaf cluster. The
// management server sends two updates for the same leaf cluster resource. The
// test verifies that the load balancing configuration pushed to the priority LB
// policy contains the expected discovery mechanism corresponding to the leaf
// test verifies that the load balancing configuration pushed to the top-level LB
Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
// policy contains the expected configuration corresponding to the leaf
// cluster, on both occasions.
func (s) TestAggregateClusterSuccess_LeafNode(t *testing.T) {
tests := []struct {
Expand All @@ -105,43 +105,21 @@ func (s) TestAggregateClusterSuccess_LeafNode(t *testing.T) {
name: "eds",
firstClusterResource: e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone),
secondClusterResource: e2e.DefaultCluster(clusterName, serviceName+"-new", e2e.SecurityLevelNone),
wantFirstChildCfg: &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterName),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
},
wantSecondChildCfg: &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-1-0": {
Config: createPriorityConfig(clusterName),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-1-0"},
},
wantFirstChildCfg: createSingleClusterConfig(clusterName, "priority-0-0", true),
wantSecondChildCfg: createSingleClusterConfig(clusterName, "priority-1-0", true),
},
{
name: "dns",
firstClusterResource: makeLogicalDNSClusterResource(clusterName, "dns_host", uint32(port)),
secondClusterResource: makeLogicalDNSClusterResource(clusterName, "dns_host_new", uint32(port)),
wantFirstChildCfg: &priority.LBConfig{
Children: map[string]*priority.Child{"priority-0": {Config: createPriorityConfig(clusterName)}},
Priorities: []string{"priority-0"},
},
wantSecondChildCfg: &priority.LBConfig{
Children: map[string]*priority.Child{"priority-1": {Config: createPriorityConfig(clusterName)}},
Priorities: []string{"priority-1"},
},
wantFirstChildCfg: createSingleClusterConfig(clusterName, "priority-0", false),
wantSecondChildCfg: createSingleClusterConfig(clusterName, "priority-1", false),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
lbCfgCh := registerWrappedOutlierDetectionPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)

// Push the first cluster resource through the management server and
Expand Down Expand Up @@ -281,11 +259,11 @@ func (s) TestAggregateClusterSuccess_ThenUpdateChildClusters(t *testing.T) {
// the priority LB policy contains the discovery mechanisms for both child
// clusters. The test then updates the root cluster resource requested by the
// cds LB policy to a leaf cluster of type EDS and verifies the load balancing
// configuration pushed to the priority LB policy contains a single discovery
// mechanism.
// configuration pushed to the outlier detection LB policy contains the leaf cluster config.
Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
func (s) TestAggregateClusterSuccess_ThenChangeRootToEDS(t *testing.T) {
dnsTargetCh, dnsR := setupDNS(t)
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
odCfgCh := registerWrappedOutlierDetectionPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)

// Configure the management server with the aggregate cluster resource
Expand Down Expand Up @@ -332,32 +310,29 @@ func (s) TestAggregateClusterSuccess_ThenChangeRootToEDS(t *testing.T) {
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
// Drain odCfgCh before sending non-aggregate update, as aggregate children pushed outlier configs into odCfgCh.
for len(odCfgCh) > 0 {
<-odCfgCh
}
Comment thread
easwars marked this conversation as resolved.
Outdated
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Since the service name of the EDS cluster remains same, same priority name
// is used.
wantChildCfg = &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterName),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
wantSingleChildCfg := createSingleClusterConfig(clusterName, "priority-0-0", true)
if err := compareLoadBalancingConfig(ctx, odCfgCh, wantSingleChildCfg); err != nil {
t.Fatal(err)
}
}

// Tests the case where a requested cluster resource switches between being a
// leaf and an aggregate cluster pointing to an EDS and LogicalDNS child
// cluster. In each of these cases, the test verifies that the load balancing
// configuration pushed to the priority LB policy contains the expected config.
// configuration pushed to the top-level child policy contains the expected config.
func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T) {
dnsTargetCh, dnsR := setupDNS(t)
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
odCfgCh := registerWrappedOutlierDetectionPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)

// Start off with the requested cluster being a leaf EDS cluster.
Expand All @@ -373,16 +348,8 @@ func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterName),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
wantSingleChildCfg := createSingleClusterConfig(clusterName, "priority-0-0", true)
if err := compareLoadBalancingConfig(ctx, odCfgCh, wantSingleChildCfg); err != nil {
t.Fatal(err)
}

Expand All @@ -404,7 +371,7 @@ func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T
}
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)

wantChildCfg = &priority.LBConfig{
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(edsClusterName),
Expand All @@ -426,19 +393,14 @@ func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
// Drain odCfgCh before sending non-aggregate update, as aggregate children pushed outlier configs into odCfgCh.
for len(odCfgCh) > 0 {
<-odCfgCh
}
Comment thread
easwars marked this conversation as resolved.
Outdated
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
wantChildCfg = &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterName),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
if err := compareLoadBalancingConfig(ctx, odCfgCh, wantSingleChildCfg); err != nil {
t.Fatal(err)
}
}
Expand Down
52 changes: 42 additions & 10 deletions internal/xds/balancer/cdsbalancer/cdsbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,20 @@ import (
const cdsName = "cds_experimental"

var (
// newChildBalancer is a helper function to build a new priority balancer
// and will be overridden in unittests.
newChildBalancer = func(cc balancer.ClientConn, opts balancer.BuildOptions) (balancer.Balancer, error) {
builder := balancer.Get(priority.Name)
// newChildBalancer is a helper function to build a new child balancer and its
// config parser, and will be overridden in unittests.
Comment thread
Pranjali-2501 marked this conversation as resolved.
Outdated
newChildBalancer = func(name string, cc balancer.ClientConn, opts balancer.BuildOptions) (balancer.Balancer, balancer.ConfigParser, error) {
builder := balancer.Get(name)
Comment thread
Pranjali-2501 marked this conversation as resolved.
if builder == nil {
return nil, fmt.Errorf("xds: no balancer builder with name %v", priority.Name)
return nil, nil, fmt.Errorf("xds: no balancer builder with name %v", name)
Comment thread
easwars marked this conversation as resolved.
Outdated
}
// We directly pass the parent clientConn to the underlying priority
parser, ok := builder.(balancer.ConfigParser)
if !ok {
return nil, nil, fmt.Errorf("xds: balancer builder for %v does not implement ConfigParser", name)
}
Comment thread
Pranjali-2501 marked this conversation as resolved.
// We directly pass the parent clientConn to the underlying child
// balancer because the cdsBalancer does not deal with subConns.
return builder.Build(cc, opts), nil
return builder.Build(cc, opts), parser, nil
}
)

Expand Down Expand Up @@ -134,6 +138,7 @@ type cdsBalancer struct {
// protect access to these fields.
xdsClient xdsclient.XDSClient
childLB balancer.Balancer // Child policy, built upon resolution of the cluster graph.
childLBName string // Name of the child policy.
clusterConfigs map[string]*xdsresource.ClusterResult // Cluster name to the last received result for that cluster.
priorityConfigs map[string]*priorityConfig // Hostname to priority config for that leaf cluster.
lbCfg *lbConfig // Current load balancing configuration.
Expand Down Expand Up @@ -267,18 +272,45 @@ func (b *cdsBalancer) handleClusterUpdate() error {
// A child policy is created if one doesn't already exist. The newly built
// configuration is then pushed to the child policy.
func (b *cdsBalancer) updateChildConfig() error {
clusterName := b.lbCfg.ClusterName
clusterConfig := b.clusterConfigs[clusterName].Config
isAggregate := clusterConfig.Cluster.ClusterType == xdsresource.ClusterTypeAggregate
Comment thread
mswierq marked this conversation as resolved.

var topLBName string
Comment thread
easwars marked this conversation as resolved.
Outdated
if isAggregate {
topLBName = priority.Name
} else {
topLBName = outlierdetection.Name
}

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

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.


if b.childLB == nil {
childLB, err := newChildBalancer(b.cc, b.bOpts)
childLB, parser, err := newChildBalancer(topLBName, b.cc, b.bOpts)
if err != nil {
return fmt.Errorf("failed to create child policy of type %s: %v", priority.Name, err)
return fmt.Errorf("failed to create child policy of type %s: %v", topLBName, err)
Comment thread
easwars marked this conversation as resolved.
Outdated
}
b.childLB = childLB
b.childLBName = topLBName
b.childConfigParser = parser
}

childCfgBytes, endpoints, err := buildPriorityConfigJSON(b.priorities, &b.xdsLBPolicy)
var childCfgBytes []byte
var endpoints []resolver.Endpoint
var err error

if isAggregate {
childCfgBytes, endpoints, err = buildAggregateClusterPriorityConfigJSON(b.priorities, &b.xdsLBPolicy)
} else {
childCfgBytes, endpoints, err = buildSingleClusterConfigJSON(b.priorities[0], &b.xdsLBPolicy)
}
Comment thread
mswierq marked this conversation as resolved.
Comment on lines +287 to +291

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)
}

if err != nil {
return fmt.Errorf("failed to build child policy config: %v", err)
}

childCfg, err := b.childConfigParser.ParseConfig(childCfgBytes)
if err != nil {
return fmt.Errorf("failed to parse child policy config. This should never happen because the config was generated: %v", err)
Expand Down
Loading
Loading