snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,932 @@
/*
* Copyright 2021 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cdsbalancer
import (
"context"
"fmt"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/pretty"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/xds/e2e"
"google.golang.org/grpc/internal/xds/balancer/priority"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
"google.golang.org/grpc/serviceconfig"
"google.golang.org/grpc/status"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
_ "google.golang.org/grpc/balancer/pickfirst"
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter.
_ "google.golang.org/grpc/internal/xds/resolver" // Register the xds resolver
)
// makeAggregateClusterResource returns an aggregate cluster resource with the
// given name and list of child names.
func makeAggregateClusterResource(name string, childNames []string) *v3clusterpb.Cluster {
return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
ClusterName: name,
Type: e2e.ClusterTypeAggregate,
ChildNames: childNames,
})
}
// makeLogicalDNSClusterResource returns a LOGICAL_DNS cluster resource with the
// given name and given DNS host and port.
func makeLogicalDNSClusterResource(name, dnsHost string, dnsPort uint32) *v3clusterpb.Cluster {
return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
ClusterName: name,
Type: e2e.ClusterTypeLogicalDNS,
DNSHostName: dnsHost,
DNSPort: dnsPort,
})
}
// verifyDNSResolution verifies that the DNS resolver is started for the expected target.
func verifyDNSResolution(ctx context.Context, t *testing.T, dnsTargetCh chan resolver.Target, dnsR *manual.Resolver, host string, port uint32) {
t.Helper()
select {
case <-ctx.Done():
t.Fatal("Timeout waiting for DNS watch")
case target := <-dnsTargetCh:
addr := fmt.Sprintf("%s:%d", host, port)
if target.Endpoint() != addr {
t.Fatalf("DNS resolution started for target %q, want %q", target.Endpoint(), addr)
}
dnsR.UpdateState(resolver.State{
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: addr}}}},
})
}
}
// 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
// cluster, on both occasions.
func (s) TestAggregateClusterSuccess_LeafNode(t *testing.T) {
tests := []struct {
name string
firstClusterResource *v3clusterpb.Cluster
secondClusterResource *v3clusterpb.Cluster
wantFirstChildCfg serviceconfig.LoadBalancingConfig
wantSecondChildCfg serviceconfig.LoadBalancingConfig
}{
{
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"},
},
},
{
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"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Push the first cluster resource through the management server and
// verify the configuration pushed to the child policy.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{test.firstClusterResource},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, test.wantFirstChildCfg); err != nil {
t.Fatal(err)
}
// Push the second cluster resource through the management server and
// verify the configuration pushed to the child policy.
resources.Clusters[0] = test.secondClusterResource
resources.Endpoints[0] = e2e.DefaultEndpoint(serviceName+"-new", host, []uint32{port})
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, test.wantSecondChildCfg); err != nil {
t.Fatal(err)
}
})
}
}
// Tests the case where the cluster resource requested is an aggregate cluster
// root pointing to two child clusters, one of type EDS and the other of type
// LogicalDNS. The test verifies that load balancing configuration is pushed to
// the priority LB policy only when all child clusters are resolved and it also
// verifies that the pushed configuration contains the expected priority config.
// The test then updates the aggregate cluster to point to two child clusters,
// the same leaf cluster of type EDS and a different leaf cluster of type
// LogicalDNS and verifies that the load balancing configuration pushed to the
// priority LB policy contains the expected config.
func (s) TestAggregateClusterSuccess_ThenUpdateChildClusters(t *testing.T) {
dnsTargetCh, dnsR := setupDNS(t)
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Configure the management server with the aggregate cluster resource
// pointing to two child clusters, one EDS and one LogicalDNS. Include the
// resource corresponding to the EDS cluster here, but don't include
// resource corresponding to the LogicalDNS cluster yet.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify that no configuration is pushed to the child policy yet, because
// not all clusters making up the aggregate cluster have been resolved yet.
select {
case cfg := <-lbCfgCh:
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
case <-time.After(defaultTestShortTimeout):
}
// Now configure the LogicalDNS cluster in the management server. This
// should result in configuration being pushed down to the child policy.
resources.Clusters = append(resources.Clusters, makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort))
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(edsClusterName),
IgnoreReresolutionRequests: true,
},
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
},
Priorities: []string{"priority-0-0", "priority-1"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
const dnsClusterNameNew = dnsClusterName + "-new"
const dnsHostNameNew = dnsHostName + "-new"
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterNameNew}),
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
makeLogicalDNSClusterResource(dnsClusterNameNew, dnsHostNameNew, dnsPort),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostNameNew, dnsPort)
wantChildCfg = &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(edsClusterName),
IgnoreReresolutionRequests: true,
},
"priority-2": {Config: createPriorityConfig(dnsClusterNameNew)},
},
Priorities: []string{"priority-0-0", "priority-2"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
}
// Tests the case where the cluster resource requested is an aggregate cluster
// root pointing to two child clusters, one of type EDS and the other of type
// LogicalDNS. The test verifies that the load balancing configuration pushed to
// 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.
func (s) TestAggregateClusterSuccess_ThenChangeRootToEDS(t *testing.T) {
dnsTargetCh, dnsR := setupDNS(t)
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Configure the management server with the aggregate cluster resource
// pointing to two child clusters.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(edsClusterName),
IgnoreReresolutionRequests: true,
},
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
},
Priorities: []string{"priority-0-0", "priority-1"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone),
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
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 {
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.
func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T) {
dnsTargetCh, dnsR := setupDNS(t)
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Start off with the requested cluster being a leaf EDS cluster.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
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 {
t.Fatal(err)
}
// Switch the requested cluster to be an aggregate cluster pointing to two
// child clusters.
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
wantChildCfg = &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(edsClusterName),
IgnoreReresolutionRequests: true,
},
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
},
Priorities: []string{"priority-0-0", "priority-1"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
// Switch the cluster back to a leaf EDS cluster.
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
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 {
t.Fatal(err)
}
}
// Tests the scenario where an aggregate cluster exceeds the maximum depth,
// which is 16. Verifies that the channel moves to TRANSIENT_FAILURE, and the
// error is propagated to RPC callers. The test then modifies the graph to no
// longer exceed maximum depth, but be at the maximum allowed depth, and
// verifies that an RPC can be made successfully.
func (s) TestAggregatedClusterFailure_ExceedsMaxStackDepth(t *testing.T) {
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{clusterName + "-1"}),
makeAggregateClusterResource(clusterName+"-1", []string{clusterName + "-2"}),
makeAggregateClusterResource(clusterName+"-2", []string{clusterName + "-3"}),
makeAggregateClusterResource(clusterName+"-3", []string{clusterName + "-4"}),
makeAggregateClusterResource(clusterName+"-4", []string{clusterName + "-5"}),
makeAggregateClusterResource(clusterName+"-5", []string{clusterName + "-6"}),
makeAggregateClusterResource(clusterName+"-6", []string{clusterName + "-7"}),
makeAggregateClusterResource(clusterName+"-7", []string{clusterName + "-8"}),
makeAggregateClusterResource(clusterName+"-8", []string{clusterName + "-9"}),
makeAggregateClusterResource(clusterName+"-9", []string{clusterName + "-10"}),
makeAggregateClusterResource(clusterName+"-10", []string{clusterName + "-11"}),
makeAggregateClusterResource(clusterName+"-11", []string{clusterName + "-12"}),
makeAggregateClusterResource(clusterName+"-12", []string{clusterName + "-13"}),
makeAggregateClusterResource(clusterName+"-13", []string{clusterName + "-14"}),
makeAggregateClusterResource(clusterName+"-14", []string{clusterName + "-15"}),
makeAggregateClusterResource(clusterName+"-15", []string{clusterName + "-16"}),
e2e.DefaultCluster(clusterName+"-16", serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
const wantErr = "aggregate cluster graph exceeds max depth"
client := testgrpc.NewTestServiceClient(cc)
_, err := client.EmptyCall(ctx, &testpb.Empty{})
if code := status.Code(err); code != codes.Unavailable {
t.Fatalf("EmptyCall() failed with code: %v, want %v", code, codes.Unavailable)
}
if err != nil && !strings.Contains(err.Error(), wantErr) {
t.Fatalf("EmptyCall() failed with err: %v, want err containing: %v", err, wantErr)
}
// Start a test service backend.
server := stubserver.StartTestService(t, nil)
t.Cleanup(server.Stop)
// Update the aggregate cluster resource to no longer exceed max depth, and
// be at the maximum depth allowed.
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterName, []string{clusterName + "-1"}),
makeAggregateClusterResource(clusterName+"-1", []string{clusterName + "-2"}),
makeAggregateClusterResource(clusterName+"-2", []string{clusterName + "-3"}),
makeAggregateClusterResource(clusterName+"-3", []string{clusterName + "-4"}),
makeAggregateClusterResource(clusterName+"-4", []string{clusterName + "-5"}),
makeAggregateClusterResource(clusterName+"-5", []string{clusterName + "-6"}),
makeAggregateClusterResource(clusterName+"-6", []string{clusterName + "-7"}),
makeAggregateClusterResource(clusterName+"-7", []string{clusterName + "-8"}),
makeAggregateClusterResource(clusterName+"-8", []string{clusterName + "-9"}),
makeAggregateClusterResource(clusterName+"-9", []string{clusterName + "-10"}),
makeAggregateClusterResource(clusterName+"-10", []string{clusterName + "-11"}),
makeAggregateClusterResource(clusterName+"-11", []string{clusterName + "-12"}),
makeAggregateClusterResource(clusterName+"-12", []string{clusterName + "-13"}),
makeAggregateClusterResource(clusterName+"-13", []string{clusterName + "-14"}),
makeAggregateClusterResource(clusterName+"-14", []string{clusterName + "-15"}),
e2e.DefaultCluster(clusterName+"-15", serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
}
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify that a successful RPC can be made.
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// Tests a diamond shaped aggregate cluster (A->[B,C]; B->D; C->D). Verifies
// that the load balancing configuration pushed to the priority LB
// policy specifies cluster D only once. Also verifies that configuration is
// pushed only after all child clusters are resolved.
func (s) TestAggregatedClusterSuccess_DiamondDependency(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Configure the management server with an aggregate cluster resource having
// a diamond dependency pattern, (A->[B,C]; B->D; C->D). Includes resources
// for cluster A, B and D, but don't include the resource for cluster C yet.
// This will help us verify that no configuration is pushed to the child
// policy until the whole cluster graph is resolved.
const (
clusterNameA = clusterName // cluster name in cds LB policy config
clusterNameB = clusterName + "-B"
clusterNameC = clusterName + "-C"
clusterNameD = clusterName + "-D"
)
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterNameA, []string{clusterNameB, clusterNameC}),
makeAggregateClusterResource(clusterNameB, []string{clusterNameD}),
e2e.DefaultCluster(clusterNameD, serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify that no configuration is pushed to the child policy yet, because
// not all clusters making up the aggregate cluster have been resolved yet.
select {
case cfg := <-lbCfgCh:
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
case <-time.After(defaultTestShortTimeout):
}
// Now configure the resource for cluster C in the management server,
// thereby completing the cluster graph. This should result in configuration
// being pushed down to the child policy.
resources.Clusters = append(resources.Clusters, makeAggregateClusterResource(clusterNameC, []string{clusterNameD}))
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterNameD),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
}
// Tests the case where the aggregate cluster graph contains duplicates (A->[B,
// C]; B->[C, D]). Verifies that the load balancing configuration pushed to the
// priority LB policy does not contain duplicates, and that the priority
// corresponding to cluster C is higher than that for cluster D. Also verifies
// that the configuration is pushed only after all child clusters are resolved.
func (s) TestAggregatedClusterSuccess_IgnoreDups(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
// Configure the management server with an aggregate cluster resource that
// has duplicates in the graph, (A->[B, C]; B->[C, D]). Include resources
// for clusters A, B and D, but don't configure the resource for cluster C
// yet. This will help us verify that no configuration is pushed to the
// child policy until the whole cluster graph is resolved.
const (
clusterNameA = clusterName // cluster name in cds LB policy config
clusterNameB = clusterName + "-B"
clusterNameC = clusterName + "-C"
clusterNameD = clusterName + "-D"
)
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterNameA, []string{clusterNameB, clusterNameC}),
makeAggregateClusterResource(clusterNameB, []string{clusterNameC, clusterNameD}),
e2e.DefaultCluster(clusterNameD, serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify that no configuration is pushed to the child policy yet, because
// not all clusters making up the aggregate cluster have been resolved yet.
select {
case cfg := <-lbCfgCh:
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
case <-time.After(defaultTestShortTimeout):
}
// Now configure the resource for cluster C in the management server,
// thereby completing the cluster graph. This should result in configuration
// being pushed down to the child policy.
resources.Clusters = append(resources.Clusters, e2e.DefaultCluster(clusterNameC, edsClusterName, e2e.SecurityLevelNone))
resources.Endpoints = append(resources.Endpoints, e2e.DefaultEndpoint(edsClusterName, "localhost", []uint32{1234}))
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterNameC),
IgnoreReresolutionRequests: true,
},
"priority-1-0": {
Config: createPriorityConfig(clusterNameD),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0", "priority-1-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
}
// Tests the scenario where the aggregate cluster graph has a node that has
// child node of itself. The case for this is A -> A, and since there is no base
// cluster (EDS or Logical DNS), no configuration should be pushed to the child
// policy. The channel is expected to move to TRANSIENT_FAILURE and RPCs are
// expected to fail with code UNAVAILABLE and an error message specifying that
// the aggregate cluster graph has no leaf clusters. Then the test updates A -> B,
// where B is a leaf EDS cluster. Verifies that configuration is pushed to the
// child policy and that an RPC can be successfully made.
func (s) TestAggregatedCluster_NodeChildOfItself(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
const (
clusterNameA = clusterName // cluster name in cds LB policy config
clusterNameB = clusterName + "-B"
)
// Configure the management server with an aggregate cluster resource whose
// child is itself.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{makeAggregateClusterResource(clusterNameA, []string{clusterNameA})},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
select {
case cfg := <-lbCfgCh:
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
case <-time.After(defaultTestShortTimeout):
}
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
// Verify that the RPC fails with expected code.
client := testgrpc.NewTestServiceClient(cc)
_, err := client.EmptyCall(ctx, &testpb.Empty{})
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
t.Fatalf("EmptyCall() failed with code: %v, want %v", gotCode, wantCode)
}
const wantErr = "aggregate cluster graph has no leaf clusters"
if !strings.Contains(err.Error(), wantErr) {
t.Fatalf("EmptyCall() failed with err: %v, want error containing %s", err, wantErr)
}
// Start a test service backend.
server := stubserver.StartTestService(t, nil)
t.Cleanup(server.Stop)
// Update the aggregate cluster to point to a leaf EDS cluster.
resources = e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
e2e.DefaultCluster(clusterNameB, serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
}
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify the configuration pushed to the child policy.
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterNameB),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
// Verify that a successful RPC can be made.
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// Tests the scenario where the aggregate cluster graph contains a cycle and
// contains no leaf clusters. The case used here is [A -> B, B -> A]. As there
// are no leaf clusters in this graph, no configuration should be pushed to the
// child policy. The channel is expected to move to TRANSIENT_FAILURE and RPCs
// are expected to fail with code UNAVAILABLE and an error message specifying
// that the aggregate cluster graph has no leaf clusters.
func (s) TestAggregatedCluster_CycleWithNoLeafNode(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
const (
clusterNameA = clusterName // cluster name in cds LB policy config
clusterNameB = clusterName + "-B"
)
// Configure the management server with an aggregate cluster resource graph
// that contains a cycle and no leaf clusters.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
makeAggregateClusterResource(clusterNameB, []string{clusterNameA}),
},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
select {
case cfg := <-lbCfgCh:
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
case <-time.After(defaultTestShortTimeout):
}
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
// Verify that the RPC fails with expected code.
client := testgrpc.NewTestServiceClient(cc)
_, err := client.EmptyCall(ctx, &testpb.Empty{})
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
t.Fatalf("EmptyCall() failed with code: %v, want %v", gotCode, wantCode)
}
const wantErr = "aggregate cluster graph has no leaf clusters"
if !strings.Contains(err.Error(), wantErr) {
t.Fatalf("EmptyCall() failed with err: %v, want %s", err, wantErr)
}
}
// Tests the scenario where the aggregate cluster graph contains a cycle and
// also contains a leaf cluster. The case used here is [A -> B, B -> A, C]. As
// there is a leaf cluster in this graph , configuration should be pushed to the
// child policy and RPCs should get routed to that leaf cluster.
func (s) TestAggregatedCluster_CycleWithLeafNode(t *testing.T) {
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
// Start a test service backend.
server := stubserver.StartTestService(t, nil)
t.Cleanup(server.Stop)
const (
clusterNameA = clusterName // cluster name in cds LB policy config
clusterNameB = clusterName + "-B"
clusterNameC = clusterName + "-C"
)
// Configure the management server with an aggregate cluster resource graph
// that contains a cycle, but also contains a leaf cluster.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
makeAggregateClusterResource(clusterNameB, []string{clusterNameA, clusterNameC}),
e2e.DefaultCluster(clusterNameC, serviceName, e2e.SecurityLevelNone),
},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Verify the configuration pushed to the child policy.
wantChildCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: createPriorityConfig(clusterNameC),
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
t.Fatal(err)
}
// Verify that a successful RPC can be made.
client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
// Tests the scenario where the cluster tree changes, and verifies that the
// watchers for the cds balancer are updated accordingly. That is the cluster
// removed from the tree no longer has a watcher and the new cluster added has a
// new watcher.
func (s) TestWatchers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
cdsResourceRequestedCh := make(chan []string, 1)
onStreamReq := func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
if req.GetTypeUrl() == version.V3ClusterURL {
if len(req.GetResourceNames()) > 0 {
select {
case cdsResourceRequestedCh <- req.GetResourceNames():
case <-ctx.Done():
}
}
}
return nil
}
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, onStreamReq)
const (
clusterA = clusterName
clusterB = clusterName + "-B"
clusterC = clusterName + "-C"
clusterD = clusterName + "-D"
)
// Initial CDS resources: A -> B, C
initialResources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterA)},
Clusters: []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterA, []string{clusterB, clusterC}),
},
SkipValidation: true,
}
if err := mgmtServer.Update(ctx, initialResources); err != nil {
t.Fatalf("Update failed: %v", err)
}
wantNames := []string{clusterA, clusterB, clusterC}
if err := waitForResourceNames(ctx, cdsResourceRequestedCh, wantNames); err != nil {
t.Fatal(err)
}
// Update the CDS resources to remove cluster C and add cluster D.
initialResources.Clusters = []*v3clusterpb.Cluster{
makeAggregateClusterResource(clusterA, []string{clusterB, clusterD}),
}
if err := mgmtServer.Update(ctx, initialResources); err != nil {
t.Fatalf("Update failed: %v", err)
}
wantNames = []string{clusterA, clusterB, clusterD}
if err := waitForResourceNames(ctx, cdsResourceRequestedCh, wantNames); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,472 @@
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package cdsbalancer implements a balancer to handle CDS responses.
package cdsbalancer
import (
"encoding/json"
"fmt"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/nop"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/pretty"
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
"google.golang.org/grpc/internal/xds/balancer/priority"
"google.golang.org/grpc/internal/xds/xdsclient"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/internal/xds/xdsdepmgr"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
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)
if builder == nil {
return nil, fmt.Errorf("xds: no balancer builder with name %v", priority.Name)
}
// We directly pass the parent clientConn to the underlying priority
// balancer because the cdsBalancer does not deal with subConns.
return builder.Build(cc, opts), nil
}
)
func init() {
balancer.Register(bb{})
}
// bb implements the balancer.Builder interface to help build a cdsBalancer.
// It also implements the balancer.ConfigParser interface to help parse the
// JSON service config, to be passed to the cdsBalancer.
type bb struct{}
// Build creates a new CDS balancer with the ClientConn.
func (bb) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
builder := balancer.Get(priority.Name)
if builder == nil {
// Shouldn't happen, registered through imported Priority builder. Still,
// defensive programming.
logger.Errorf("%q LB policy is needed but not registered", priority.Name)
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy is needed but not registered", priority.Name))
}
parser, ok := builder.(balancer.ConfigParser)
if !ok {
// Shouldn't happen, imported Priority builder has this method.
logger.Errorf("%q LB policy does not implement a config parser", priority.Name)
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy does not implement a config parser", priority.Name))
}
b := &cdsBalancer{
bOpts: opts,
childConfigParser: parser,
clusterConfigs: make(map[string]*xdsresource.ClusterResult),
priorityConfigs: make(map[string]*priorityConfig),
cc: cc,
}
b.logger = prefixLogger(b)
b.logger.Infof("Created")
return b
}
// Name returns the name of balancers built by this builder.
func (bb) Name() string {
return cdsName
}
// lbConfig represents the loadBalancingConfig section of the service config
// for the cdsBalancer.
type lbConfig struct {
serviceconfig.LoadBalancingConfig
ClusterName string `json:"cluster"`
IsDynamic bool `json:"isDynamic"`
}
// ParseConfig parses the JSON load balancer config provided into an
// internal form or returns an error if the config is invalid.
func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
var cfg lbConfig
if err := json.Unmarshal(c, &cfg); err != nil {
return nil, fmt.Errorf("xds: unable to unmarshal lbconfig: %s, error: %v", string(c), err)
}
return &cfg, nil
}
// cdsBalancer implements a CDS based LB policy. It instantiates a
// cluster_resolver balancer to further resolve the serviceName received from
// CDS, into localities and endpoints. Implements the balancer.Balancer
// interface which is exposed to gRPC and implements the balancer.ClientConn
// interface which is exposed to the cluster_resolver balancer.
type cdsBalancer struct {
// The following fields are initialized at build time and are either
// read-only after that or provide their own synchronization, and therefore
// 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.
logger *grpclog.PrefixLogger // Prefix logger for all logging.
// All fields below are accessed only from methods implementing the
// balancer.Balancer interface. Since gRPC guarantees that these methods are
// never invoked concurrently, no additional synchronization is required to
// protect access to these fields.
xdsClient xdsclient.XDSClient
childLB balancer.Balancer // Child policy, built upon resolution of the cluster graph.
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.
priorities []*priorityConfig // List of priorities in the order.
unsubscribe func() // For dynamic cluster unsubscription.
isSubscribed bool // True if a dynamic cluster has been subscribed to.
clusterSubscriber xdsdepmgr.ClusterSubscriber // To subscribe to dynamic cluster resource.
xdsLBPolicy internalserviceconfig.BalancerConfig // Stores the locality and endpoint picking policy.
attributes *attributes.Attributes // Attributes from resolver state.
serviceConfig *serviceconfig.ParseResult
// Each new leaf cluster needs a child name generator to reuse child policy
// names. But to make sure the names across leaf clusters doesn't conflict,
// we need a seq ID. This ID is incremented for each new cluster.
childNameGeneratorSeqID uint64
}
// UpdateClientConnState receives the serviceConfig, xdsConfig,
// ClusterSubscriber and the xdsClient object from the xdsResolver. If an error
// is encountered, the parent (clustermanager) sets the corresponding cluster’s
// picker to transient_failure. Otherwise, the received configuration is
// processed and forwarded to the appropriate child policy.
func (b *cdsBalancer) UpdateClientConnState(state balancer.ClientConnState) error {
if b.xdsClient == nil {
c := xdsclient.FromResolverState(state.ResolverState)
if c == nil {
b.logger.Warningf("Received balancer config with no xDS client")
return balancer.ErrBadResolverState
}
b.xdsClient = c
}
b.logger.Infof("Received balancer config update: %s", pretty.ToJSON(state.BalancerConfig))
xdsConfig := xdsresource.XDSConfigFromResolverState(state.ResolverState)
if xdsConfig == nil {
b.logger.Warningf("Received balancer config with no xDS config")
return balancer.ErrBadResolverState
}
b.clusterConfigs = xdsConfig.Clusters
b.clusterSubscriber = xdsdepmgr.XDSClusterSubscriberFromResolverState(state.ResolverState)
if b.clusterSubscriber == nil {
b.logger.Warningf("Received balancer config with no cluster subscriber")
return balancer.ErrBadResolverState
}
// The errors checked here should ideally never happen because the
// ServiceConfig in this case is prepared by the xdsResolver and is not
// something that is received on the wire.
lbCfg, ok := state.BalancerConfig.(*lbConfig)
if !ok {
b.logger.Warningf("Received unexpected balancer config type: %T", state.BalancerConfig)
return balancer.ErrBadResolverState
}
if lbCfg.ClusterName == "" {
b.logger.Warningf("Received balancer config with no cluster name")
return balancer.ErrBadResolverState
}
b.lbCfg = lbCfg
b.serviceConfig = state.ResolverState.ServiceConfig
b.attributes = state.ResolverState.Attributes
return b.handleXDSConfigUpdate()
}
// handleXDSConfigUpdate processes the XDSConfig update from the xDS resolver.
func (b *cdsBalancer) handleXDSConfigUpdate() error {
clusterName := b.lbCfg.ClusterName
// If the cluster is dynamic and we dont have a subscription yet, create
// one.
if b.lbCfg.IsDynamic && !b.isSubscribed {
b.unsubscribe = b.clusterSubscriber.SubscribeToCluster(clusterName)
b.isSubscribed = true
return nil
}
clusterUpdate, ok := b.clusterConfigs[clusterName]
if !ok {
// If the cluster is missing from the config, check if it is dynamic.
// For dynamic clusters, the xDS config may be updated before the
// corresponding cluster resource is received. This should never occur
// for static clusters.
if b.lbCfg.IsDynamic {
return nil
}
return b.annotateErrorWithNodeID(fmt.Errorf("did not find the cluster %q in XDSConfig", clusterName))
}
// If the cluster resource has an error, return the error.
if clusterUpdate.Err != nil {
return clusterUpdate.Err
}
return b.handleClusterUpdate()
}
// handleClusterUpdate handles a good XDSConfig update from the xDS resolver.
// Builds the child policy config and pushes it down.
func (b *cdsBalancer) handleClusterUpdate() error {
clusterName := b.lbCfg.ClusterName
clusterConfig := b.clusterConfigs[clusterName].Config
var newPriorities []*priorityConfig
switch clusterConfig.Cluster.ClusterType {
case xdsresource.ClusterTypeEDS, xdsresource.ClusterTypeLogicalDNS:
p := b.updatePriorityConfig(clusterName, &clusterConfig)
newPriorities = append(newPriorities, p)
case xdsresource.ClusterTypeAggregate:
for _, leaf := range clusterConfig.AggregateConfig.LeafClusters {
leafCluster := b.clusterConfigs[leaf]
// Update priority config for leaf clusters.
p := b.updatePriorityConfig(leaf, &leafCluster.Config)
newPriorities = append(newPriorities, p)
}
}
b.priorities = newPriorities
if err := b.updateOutlierDetection(); err != nil {
return b.annotateErrorWithNodeID(fmt.Errorf("failed to correctly update Outlier Detection config %v", err))
}
// The LB policy is configured by the root cluster.
if err := json.Unmarshal(clusterConfig.Cluster.LBPolicy, &b.xdsLBPolicy); err != nil {
return b.annotateErrorWithNodeID(fmt.Errorf("error unmarshalling xDS LB Policy: %v", err))
}
if err := b.updateChildConfig(); err != nil {
return b.annotateErrorWithNodeID(err)
}
return nil
}
// updateChildConfig builds child policy configuration using endpoint addresses
// returned from the XDSConfig and child policy configuration.
//
// 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 {
if b.childLB == nil {
childLB, err := newChildBalancer(b.cc, b.bOpts)
if err != nil {
return fmt.Errorf("failed to create child policy of type %s: %v", priority.Name, err)
}
b.childLB = childLB
}
childCfgBytes, endpoints, err := buildPriorityConfigJSON(b.priorities, &b.xdsLBPolicy)
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)
}
if b.logger.V(2) {
b.logger.Infof("Built child policy config: %s", pretty.ToJSON(childCfg))
}
for i := range endpoints {
for j := range endpoints[i].Addresses {
addr := endpoints[i].Addresses[j]
addr.BalancerAttributes = endpoints[i].Attributes
// BalancerAttributes are used for the following:
// * Authority Override.
// * grpc.lb.backend_service metric label propagation.
// See https://github.com/grpc/grpc-go/issues/6472
endpoints[i].Addresses[j] = addr
}
}
if err := b.childLB.UpdateClientConnState(balancer.ClientConnState{
ResolverState: resolver.State{
Endpoints: endpoints,
ServiceConfig: b.serviceConfig,
Attributes: b.attributes,
},
BalancerConfig: childCfg,
}); err != nil {
return fmt.Errorf("failed to push config to child policy: %v", err)
}
return nil
}
// updatePriorityConfig updates the priority configuration for the specified EDS
// or DNS cluster, creating it if it does not already exist.
func (b *cdsBalancer) updatePriorityConfig(clusterName string, clusterConfig *xdsresource.ClusterConfig) *priorityConfig {
name := hostName(clusterName, *clusterConfig.Cluster)
pc, ok := b.priorityConfigs[name]
if !ok {
pc = &priorityConfig{
childNameGen: newNameGenerator(b.childNameGeneratorSeqID),
}
b.priorityConfigs[name] = pc
// Increment the seq ID for the next new cluster. This is done to make
// sure that the child policy names generated for different clusters
// don't conflict with each other.
b.childNameGeneratorSeqID++
}
pc.clusterConfig = clusterConfig
return pc
}
// updateOutlierDetection updates Outlier Detection config for all priorities.
func (b *cdsBalancer) updateOutlierDetection() error {
odBuilder := balancer.Get(outlierdetection.Name)
if odBuilder == nil {
// Shouldn't happen, registered through imported Outlier Detection,
// defensive programming.
return fmt.Errorf("%q LB policy is needed but not registered", outlierdetection.Name)
}
odParser, ok := odBuilder.(balancer.ConfigParser)
if !ok {
// Shouldn't happen, imported Outlier Detection builder has this method.
return fmt.Errorf("%q LB policy does not implement a config parser", outlierdetection.Name)
}
for _, p := range b.priorities {
// Update Outlier Detection Config.
odJSON := p.clusterConfig.Cluster.OutlierDetection
if odJSON == nil {
odJSON = json.RawMessage(`{}`)
}
lbCfg, err := odParser.ParseConfig(odJSON)
if err != nil {
return fmt.Errorf("error parsing Outlier Detection config %v: %v", odJSON, err)
}
odCfg, ok := lbCfg.(*outlierdetection.LBConfig)
if !ok {
// Shouldn't happen, Parser built at build time with Outlier
// Detection builder pulled from gRPC LB Registry.
return fmt.Errorf("config parser for Outlier Detection returned config with unexpected type %T: %v", lbCfg, lbCfg)
}
p.outlierDetection = *odCfg
}
return nil
}
// ResolverError handles errors reported by the xdsResolver.
func (b *cdsBalancer) ResolverError(err error) {
// Missing Listener or RouteConfiguration on the management server
// results in a 'resource not found' error from the xDS resolver. In
// these cases, we should report transient failure.
if xdsresource.ErrType(err) == xdsresource.ErrorTypeResourceNotFound {
b.closeChildPolicyAndReportTF(err)
return
}
var root string
if b.lbCfg != nil {
root = b.lbCfg.ClusterName
}
b.onClusterError(root, err)
}
// UpdateSubConnState handles subConn updates from gRPC.
func (b *cdsBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
}
// closeChildPolicyAndReportTF closes the child policy, if it exists, and
// updates the connectivity state of the channel to TransientFailure with an
// error picker.
func (b *cdsBalancer) closeChildPolicyAndReportTF(err error) {
if b.childLB != nil {
b.childLB.Close()
b.childLB = nil
}
b.cc.UpdateState(balancer.State{
ConnectivityState: connectivity.TransientFailure,
Picker: base.NewErrPicker(err),
})
}
// Close closes the child policy, unsubscribes to the dynamic cluster, and
// closes the cdsBalancer.
func (b *cdsBalancer) Close() {
if b.childLB != nil {
b.childLB.Close()
b.childLB = nil
}
if b.unsubscribe != nil {
b.unsubscribe()
}
b.logger.Infof("Shutdown")
}
func (b *cdsBalancer) ExitIdle() {
if b.childLB == nil {
b.logger.Warningf("Received ExitIdle with no child policy")
return
}
// This implementation assumes the child balancer supports
// ExitIdle (but still checks for the interface's existence to
// avoid a panic if not). If the child does not, no subconns
// will be connected.
b.childLB.ExitIdle()
}
// Node ID needs to be manually added to errors generated in the following
// scenarios:
// - resource-does-not-exist: since the xDS watch API uses a separate callback
// instead of returning an error value. TODO(gRFC A88): Once A88 is
// implemented, the xDS client will be able to add the node ID to
// resource-does-not-exist errors as well, and we can get rid of this
// special handling.
// - received a good update from the xDS client, but the update either contains
// an invalid security configuration or contains invalid aggragate cluster
// config.
func (b *cdsBalancer) annotateErrorWithNodeID(err error) error {
nodeID := b.xdsClient.BootstrapConfig().Node().GetId()
return fmt.Errorf("[xDS node id: %v]: %w", nodeID, err)
}
// onClusterAmbientError handles an ambient error, if a childLB already has a
// good update, it should continue using that.
func (b *cdsBalancer) onClusterAmbientError(name string, err error) {
b.logger.Warningf("Cluster resource %q received ambient error update: %v", name, err)
if xdsresource.ErrType(err) != xdsresource.ErrorTypeConnection && b.childLB != nil {
// Connection errors will be sent to the child balancers directly.
// There's no need to forward them.
b.childLB.ResolverError(err)
}
}
// onClusterResourceError handles errors to stop using the previously seen
// resource. Propagates the error down to the child policy if one exists, and
// puts the channel in TRANSIENT_FAILURE.
func (b *cdsBalancer) onClusterResourceError(name string, err error) {
b.logger.Warningf("CDS watch for resource %q reported resource error", name)
b.closeChildPolicyAndReportTF(err)
}
func (b *cdsBalancer) onClusterError(name string, err error) {
if b.childLB != nil {
b.onClusterAmbientError(name, err)
} else {
b.onClusterResourceError(name, err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,375 @@
/*
*
* Copyright 2021 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cdsbalancer
import (
"encoding/json"
"fmt"
"maps"
"slices"
"google.golang.org/grpc/experimental/balancer/hostname"
"google.golang.org/grpc/experimental/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/hierarchy"
"google.golang.org/grpc/internal/proxyattributes"
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
xdsinternal "google.golang.org/grpc/internal/xds"
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
"google.golang.org/grpc/internal/xds/balancer/priority"
"google.golang.org/grpc/internal/xds/balancer/wrrlocality"
"google.golang.org/grpc/internal/xds/clients"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/resolver"
)
const million = 1000000
// priorityConfig is config for one priority. For example, if there's an EDS and
// a DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}].
//
// Each priorityConfig corresponds to one leaf cluster retrieved from XDSConfig
// for the top-level cluster.
type priorityConfig struct {
// clusterConfig has the cluster update as well as EDS or DNS endpoints
// depending on the leaf cluster type.
clusterConfig *xdsresource.ClusterConfig
// outlierDetection is the Outlier Detection LB configuration for this
// priority.
outlierDetection outlierdetection.LBConfig
// Each leaf cluster has a name generator so that the child policies can
// reuse names between updates (EDS updates for example).
childNameGen *nameGenerator
}
// hostName returns the name of the host for the given cluster.
//
// For EDS, it's the EDSServiceName (or ClusterName if empty).
// For DNS, it's the DNSHostName.
func hostName(clusterName string, update xdsresource.ClusterUpdate) string {
switch update.ClusterType {
case xdsresource.ClusterTypeEDS:
if update.EDSServiceName != "" {
return update.EDSServiceName
}
return clusterName
case xdsresource.ClusterTypeLogicalDNS:
return update.DNSHostName
default:
return ""
}
}
// buildPriorityConfigJSON builds balancer config for the passed in
// priorities.
//
// The built tree of balancers (see test for the output struct).
//
// ┌────────┐
// │priority│
// └┬──────┬┘
// │ │
// ┌──────────▼─┐ ┌─▼──────────┐
// │cluster_impl│ │cluster_impl│
// └──────┬─────┘ └─────┬──────┘
// │ │
// ┌──────▼─────┐ ┌─────▼──────┐
// │xDSLBPolicy │ │xDSLBPolicy │ (Locality and Endpoint picking layer)
// └────────────┘ └────────────┘
func buildPriorityConfigJSON(priorities []*priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Endpoint, error) {
pc, endpoints, err := buildPriorityConfig(priorities, xdsLBPolicy)
if err != nil {
return nil, nil, fmt.Errorf("failed to build priority config: %v", err)
}
ret, err := json.Marshal(pc)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err)
}
return ret, endpoints, nil
}
func buildPriorityConfig(priorities []*priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Endpoint, error) {
var (
retConfig = &priority.LBConfig{Children: make(map[string]*priority.Child)}
retEndpoints []resolver.Endpoint
)
for _, p := range priorities {
clusterUpdate := p.clusterConfig.Cluster
switch clusterUpdate.ClusterType {
case xdsresource.ClusterTypeEDS:
names, configs, endpoints, err := buildClusterImplConfigForEDS(p.childNameGen, p.clusterConfig, xdsLBPolicy)
if err != nil {
return nil, nil, err
}
retConfig.Priorities = append(retConfig.Priorities, names...)
retEndpoints = append(retEndpoints, endpoints...)
odCfgs := convertClusterImplMapToOutlierDetection(configs, p.outlierDetection)
for n, c := range odCfgs {
retConfig.Children[n] = &priority.Child{
Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: c},
// Ignore all re-resolution from EDS children.
IgnoreReresolutionRequests: true,
}
}
continue
case xdsresource.ClusterTypeLogicalDNS:
name, config, endpoints := buildClusterImplConfigForDNS(p.childNameGen, p.clusterConfig, xdsLBPolicy)
retConfig.Priorities = append(retConfig.Priorities, name)
retEndpoints = append(retEndpoints, endpoints...)
odCfg := makeClusterImplOutlierDetectionChild(config, p.outlierDetection)
retConfig.Children[name] = &priority.Child{
Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: odCfg},
// Not ignore re-resolution from DNS children, they will trigger
// DNS to re-resolve.
IgnoreReresolutionRequests: false,
}
continue
}
}
return retConfig, retEndpoints, nil
}
func convertClusterImplMapToOutlierDetection(ciCfgs map[string]*clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) map[string]*outlierdetection.LBConfig {
odCfgs := make(map[string]*outlierdetection.LBConfig, len(ciCfgs))
for n, c := range ciCfgs {
odCfgs[n] = makeClusterImplOutlierDetectionChild(c, odCfg)
}
return odCfgs
}
func makeClusterImplOutlierDetectionChild(ciCfg *clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) *outlierdetection.LBConfig {
odCfgRet := odCfg
odCfgRet.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: ciCfg}
return &odCfgRet
}
func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (string, *clusterimpl.LBConfig, []resolver.Endpoint) {
pName := fmt.Sprintf("priority-%v", g.prefix)
clusterUpdate := config.Cluster
lbconfig := &clusterimpl.LBConfig{
Cluster: clusterUpdate.ClusterName,
ChildPolicy: xdsLBPolicy,
}
endpoints := config.EndpointConfig.DNSEndpoints.Endpoints
if len(endpoints) == 0 {
return pName, lbconfig, nil
}
var retEndpoint resolver.Endpoint
for _, e := range endpoints {
// LOGICAL_DNS requires all resolved addresses to be grouped into a
// single logical endpoint. We iterate over the input endpoints and
// aggregate their addresses into a new endpoint variable.
retEndpoint.Addresses = append(retEndpoint.Addresses, e.Addresses...)
}
// Even though localities are not a thing for the LOGICAL_DNS cluster and
// its endpoint(s), we add an empty locality attribute here to ensure that
// LB policies that rely on locality information (like weighted_target)
// continue to work.
localityStr := xdsinternal.LocalityString(clients.Locality{})
retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
// Set the locality weight to 1. This is required because the child policy
// like weighted_target which relies on locality weights to distribute
// traffic. These policies may drop traffic if the weight is 0.
retEndpoint = wrrlocality.SetAddrInfo(retEndpoint, wrrlocality.AddrInfo{LocalityWeight: 1})
return pName, lbconfig, []resolver.Endpoint{retEndpoint}
}
// buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for
// each priority, sorted by priority, and the addresses for each priority (with
// hierarchy attributes set).
//
// For example, if there are two priorities, the returned values will be
// - ["p0", "p1"]
// - map{"p0":p0_config, "p1":p1_config}
// - [p0_address_0, p0_address_1, p1_address_0, p1_address_1]
// - p0 addresses' hierarchy attributes are set to p0
func buildClusterImplConfigForEDS(g *nameGenerator, config *xdsresource.ClusterConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Endpoint, error) {
edsUpdate := config.EndpointConfig.EDSUpdate
// Localities of length 0 is triggered by an NACK or resource-not-found
// error before update, or an empty localities list in an update. In either
// case want to create a priority, and send down empty address list, causing
// TF for that priority. "If any discovery mechanism instance experiences an
// error retrieving data, and it has not previously reported any results, it
// should report a result that is a single priority with no endpoints." -
// A37
priorities := [][]xdsresource.Locality{{}}
if len(edsUpdate.Localities) != 0 {
priorities = groupLocalitiesByPriority(edsUpdate.Localities)
}
retNames := g.generate(priorities)
retConfigs := make(map[string]*clusterimpl.LBConfig, len(retNames))
var retEndpoints []resolver.Endpoint
for i, pName := range retNames {
priorityLocalities := priorities[i]
cfg, endpoints, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, *config.Cluster, xdsLBPolicy)
if err != nil {
return nil, nil, nil, err
}
retConfigs[pName] = cfg
retEndpoints = append(retEndpoints, endpoints...)
}
return retNames, retConfigs, retEndpoints, nil
}
// groupLocalitiesByPriority returns the localities grouped by priority.
//
// The returned list is sorted from higher priority to lower. Each item in the
// list is a group of localities.
//
// For example, for L0-p0, L1-p0, L2-p1, results will be
// - [[L0, L1], [L2]]
func groupLocalitiesByPriority(localities []xdsresource.Locality) [][]xdsresource.Locality {
priorities := make(map[int][]xdsresource.Locality)
for _, locality := range localities {
priority := int(locality.Priority)
priorities[priority] = append(priorities[priority], locality)
}
// Sort the priorities based on the int value, deduplicate, and then turn
// the sorted list into a string list. This will be child names, in priority
// order.
priorityIntSlice := slices.Sorted(maps.Keys(priorities))
ret := make([][]xdsresource.Locality, 0, len(priorityIntSlice))
for _, p := range priorityIntSlice {
ret = append(ret, priorities[p])
}
return ret
}
// priorityLocalitiesToClusterImpl takes a list of localities (with the same
// priority), and generates a cluster impl policy config, and a list of
// addresses with their path hierarchy set to [priority-name, locality-name], so
// priority and the xDS LB Policy know which child policy each address is for.
func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priorityName string, clusterUpdate xdsresource.ClusterUpdate, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Endpoint, error) {
var retEndpoints []resolver.Endpoint
// Compute the sum of locality weights to normalize locality weights. The
// xDS client guarantees that the sum of locality weights (within a
// priority) will not exceed MaxUint32.
var localityWeightSum uint32
for _, locality := range localities {
localityWeightSum += locality.Weight
}
for _, locality := range localities {
// Compute the sum of endpoint weights to normalize endpoint weights.
// The xDS client does not currently guarantee that the sum of endpoint
// weights (within a locality) will not exceed MaxUint32. TODO(i/8862):
// Once the xDS client guarantees that the sum of endpoint weights does
// not exceed MaxUint32, we can change the type of this variable from
// uint64 to uint32.
var endpointWeightSum uint64
for _, endpoint := range locality.Endpoints {
endpointWeightSum += uint64(endpoint.Weight)
}
localityStr := xdsinternal.LocalityString(locality.ID)
for _, endpoint := range locality.Endpoints {
// Filter out all "unhealthy" endpoints (unknown and healthy are
// both considered to be healthy:
// https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus).
if endpoint.HealthStatus != xdsresource.EndpointHealthStatusHealthy && endpoint.HealthStatus != xdsresource.EndpointHealthStatusUnknown {
continue
}
// Create a copy of endpoint.ResolverEndpoint to avoid race between
// the xDS Client (which owns this shared object in its cache) and
// the Cluster Resolver (which is trying to modify attributes).
resolverEndpoint := endpoint.ResolverEndpoint
resolverEndpoint.Addresses = slices.Clone(endpoint.ResolverEndpoint.Addresses)
if clusterUpdate.IsHTTP11ProxyEnabled {
var proxyAddrStr string
if val, ok := endpoint.Metadata["envoy.http11_proxy_transport_socket.proxy_address"].(xdsresource.ProxyAddressMetadataValue); ok {
proxyAddrStr = val.Address
} else if val, ok := locality.Metadata["envoy.http11_proxy_transport_socket.proxy_address"].(xdsresource.ProxyAddressMetadataValue); ok {
proxyAddrStr = val.Address
}
if proxyAddrStr != "" {
for idx, addr := range resolverEndpoint.Addresses {
connectAddr := addr.Addr
addr.Addr = proxyAddrStr
resolverEndpoint.Addresses[idx] = proxyattributes.Set(addr, proxyattributes.Options{
ConnectAddr: connectAddr,
})
}
}
}
resolverEndpoint = hierarchy.SetInEndpoint(resolverEndpoint, []string{priorityName, localityStr})
resolverEndpoint = xdsinternal.SetLocalityIDInEndpoint(resolverEndpoint, locality.ID)
// "To provide the xds_wrr_locality load balancer information about
// locality weights received from EDS, the cluster resolver will
// populate a new locality weight attribute for each address The
// attribute will have the weight (as an integer) of the locality
// the address is part of." - A52
resolverEndpoint = wrrlocality.SetAddrInfo(resolverEndpoint, wrrlocality.AddrInfo{LocalityWeight: locality.Weight})
if envconfig.PickFirstWeightedShuffling {
normalizedLocalityWeight := fractionToFixedPoint(uint64(locality.Weight), uint64(localityWeightSum))
normalizedEndpointWeight := fractionToFixedPoint(uint64(endpoint.Weight), endpointWeightSum)
endpointWeight := fixedPointMultiply(normalizedEndpointWeight, normalizedLocalityWeight)
if endpointWeight == 0 {
endpointWeight = 1
}
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: endpointWeight})
} else {
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: locality.Weight * endpoint.Weight})
}
retEndpoints = append(retEndpoints, resolverEndpoint)
}
}
return &clusterimpl.LBConfig{
Cluster: clusterUpdate.ClusterName,
ChildPolicy: xdsLBPolicy,
}, retEndpoints, nil
}
// fixedPointFractionalBits is the number of bits used for the fractional part
// of normalized endpoint and locality weights.
//
// We use the UQ1.31 fixed-point format (Unsigned, 1 integer bit, 31 fractional bits).
// This allows representing values in the range [0.0, 2.0) with a precision
// of 2^-31.
//
// Bit Layout:
// [ 31 ] [ 30 ................. 0 ]
//
// | |
// | +--- Fractional Part (31 bits)
// +------------------ Integer Part (1 bit)
//
// See gRFC A113 for more details.
const fixedPointFractionalBits = 31
// fractionToFixedPoint converts a fraction represented by numerator and
// denominator to a fixed-point number between 0 and 1 represented as a uint32.
//
// The xDS client guarantees that the sum of locality weights (within a
// priority) will not exceed MaxUint32. TODO(i/8862): Once the xDS client
// guarantees that the sum of endpoint weights does not exceed MaxUint32, we can
// change the types of this function's arguments from uint64 to uint32.
func fractionToFixedPoint(numerator, denominator uint64) uint32 {
return uint32(uint64(numerator) << fixedPointFractionalBits / uint64(denominator))
}
func fixedPointMultiply(a, b uint32) uint32 {
return uint32((uint64(a) * uint64(b)) >> fixedPointFractionalBits)
}

View File

@@ -0,0 +1,111 @@
/*
*
* Copyright 2022 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cdsbalancer
import (
"fmt"
"google.golang.org/grpc/internal/xds/clients"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
)
// nameGenerator generates a child name for a list of priorities (each priority
// is a list of localities).
//
// The purpose of this generator is to reuse names between updates. So the
// struct keeps state between generate() calls, and a later generate() might
// return names returned by the previous call.
type nameGenerator struct {
prevLocalitiesToChildNames map[clients.Locality]string // locality to child name mapping generated for the previous update
prevChildNames []string // prioritized list of child names generated for the previous update
prefix uint64
nextID uint64
}
func newNameGenerator(prefix uint64) *nameGenerator {
return &nameGenerator{prefix: prefix}
}
// generate returns a list of names for the given list of priorities.
//
// Each priority is a list of localities. The name for the priority is picked as
// - for each locality in this priority, if it exists in the existing names,
// this priority will reuse the name
// - if no reusable name is found for this priority, a new name is generated
//
// For example:
// - update 1: [[L1], [L2], [L3]] --> ["0", "1", "2"]
// - update 2: [[L1], [L2], [L3]] --> ["0", "1", "2"]
// - update 3: [[L1, L2], [L3]] --> ["0", "2"] (Two priorities were merged)
// - update 4: [[L1], [L4]] --> ["0", "3",] (A priority was split, and a new priority was added)
func (ng *nameGenerator) generate(priorities [][]xdsresource.Locality) []string {
ret := make([]string, len(priorities))
usedNames := make(map[string]bool)
newNames := make(map[clients.Locality]string)
// Pass 1: Same priority index match.
for i, priority := range priorities {
if i >= len(ng.prevChildNames) {
continue
}
targetName := ng.prevChildNames[i]
for _, locality := range priority {
if name, ok := ng.prevLocalitiesToChildNames[locality.ID]; ok && name == targetName {
ret[i] = targetName
usedNames[targetName] = true
break
}
}
}
// Pass 2: Greedy reuse.
for i, priority := range priorities {
if ret[i] != "" {
continue
}
for _, locality := range priority {
if name, ok := ng.prevLocalitiesToChildNames[locality.ID]; ok {
if !usedNames[name] {
ret[i] = name
usedNames[name] = true
break
}
}
}
}
// Pass 3: New name.
for i, name := range ret {
if name == "" {
newID := fmt.Sprintf("priority-%d-%d", ng.prefix, ng.nextID)
ng.nextID++
ret[i] = newID
usedNames[newID] = true
}
}
// Update state.
for i, priority := range priorities {
for _, l := range priority {
newNames[l.ID] = ret[i]
}
}
ng.prevLocalitiesToChildNames = newNames
ng.prevChildNames = ret
return ret
}

View File

@@ -0,0 +1,311 @@
/*
*
* Copyright 2022 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cdsbalancer
import (
"testing"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/xds/clients"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
)
func (s) Test_nameGenerator_generate(t *testing.T) {
tests := []struct {
name string
prefix uint64
steps []struct {
input [][]xdsresource.Locality
want []string
}
}{
{
name: "init, two new priorities",
prefix: 3,
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-3-0", "priority-3-1"},
},
},
},
{
name: "one new priority",
prefix: 1,
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
},
want: []string{"priority-1-0"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-1-0", "priority-1-1"},
},
},
},
{
name: "merge two priorities",
prefix: 4,
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-4-0", "priority-4-1", "priority-4-2"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-4-0", "priority-4-2"},
},
},
},
{
name: "swap two priorities",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-0-0", "priority-0-1", "priority-0-2"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-0-1", "priority-0-0", "priority-0-2"},
},
},
},
{
name: "split priority",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}},
},
want: []string{"priority-0-0", "priority-0-2", "priority-0-1"},
},
},
},
{
name: "priority index preference",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L0"}}},
},
want: []string{"priority-0-0"},
},
},
},
{
name: "merge partial",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
},
},
{
name: "swap shift",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-0-1", "priority-0-0"},
},
},
},
{
name: "replace priority",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
{{ID: clients.Locality{Zone: "L5"}}},
},
want: []string{"priority-0-0", "priority-0-2"},
},
},
},
{
name: "reordered merge",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L3"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
},
},
{
name: "three-step shift stability",
steps: []struct {
input [][]xdsresource.Locality
want []string
}{
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L2"}}},
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-0-2", "priority-0-0", "priority-0-1"},
},
{
input: [][]xdsresource.Locality{
{{ID: clients.Locality{Zone: "L0"}}},
{{ID: clients.Locality{Zone: "L1"}}},
},
want: []string{"priority-0-0", "priority-0-1"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ng := newNameGenerator(tt.prefix)
for i, step := range tt.steps {
got := ng.generate(step.input)
if diff := cmp.Diff(got, step.want); diff != "" {
t.Errorf("step %d: generate() = got: %v, want: %v, diff (-got +want): %s", i, got, step.want, diff)
}
}
})
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,420 @@
/*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package e2e_test
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/balancer/stub"
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/xds/e2e"
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
"google.golang.org/grpc/internal/xds/balancer/priority"
"google.golang.org/grpc/internal/xds/balancer/wrrlocality"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
_ "google.golang.org/grpc/internal/xds/balancer/cdsbalancer" // Register the "cds_experimental" LB policy.
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter
_ "google.golang.org/grpc/internal/xds/resolver" // Register the xds resolver
)
// setupAndDial performs common setup across all tests
//
// - creates an xDS client with the passed in bootstrap contents
// - creates a xds resolver to be used
// - creates a ClientConn to talk to the test backends
//
// Returns a function to close the ClientConn and the xDS client.
func setupAndDial(t *testing.T, bootstrapContents []byte) (*grpc.ClientConn, func()) {
t.Helper()
// Create an xDS resolver with the above bootstrap configuration.
r, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
if err != nil {
t.Fatalf("xDS resolver creation failed: %v", err)
}
// Create a ClientConn and make a successful RPC.
cc, err := grpc.NewClient("xds:///"+serviceName, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
if err != nil {
t.Fatalf("grpc.NewClient() failed: %v", err)
}
cc.Connect()
return cc, func() {
cc.Close()
}
}
// TestErrorFromParentLB_ConnectionError tests the case where a connection error
// is sent by the CDS LB Policy. The CDS LB policy sends a connection error when
// the ADS stream to the management server breaks. The test verifies that there
// is no perceivable effect because of this connection error, and that RPCs
// continue to work (because the LB policies are expected to use previously
// received xDS resources).
func (s) TestErrorFromParentLB_ConnectionError(t *testing.T) {
// Create a listener to be used by the management server. The test will
// close this listener to simulate ADS stream breakage.
lis, err := testutils.LocalTCPListener()
if err != nil {
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
}
// Start an xDS management server with the above restartable listener, and
// push a channel when the stream is closed.
streamClosedCh := make(chan struct{}, 1)
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
Listener: lis,
OnStreamClosed: func(int64, *v3corepb.Node) {
select {
case streamClosedCh <- struct{}{}:
default:
}
},
})
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
server := stubserver.StartTestService(t, nil)
defer server.Stop()
resources := e2e.DefaultClientResources(e2e.ResourceParams{
NodeID: nodeID,
DialTarget: serviceName,
Host: "localhost",
Port: testutils.ParsePort(t, server.Address),
})
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Create xDS client, xdsResolver, and dial the test backends.
cc, cleanup := setupAndDial(t, bootstrapContents)
defer cleanup()
client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
// Close the listener and ensure that the ADS stream breaks.
lis.Close()
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for ADS stream to close")
default:
}
// Ensure that RPCs continue to succeed for the next second.
for end := time.Now().Add(time.Second); time.Now().Before(end); <-time.After(defaultTestShortTimeout) {
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
}
}
// TestErrorFromParentLB_ResourceNotFound tests the case where a
// resource-not-found error is received by the CDS LB policy for a cluster. The
// resource-not-found error is received when the cluster resource associated
// with these LB policies is removed by the management server. The test verifies
// that the associated EDS is canceled and RPCs fail. It also ensures that when
// the Cluster resource is added back, the EDS resource is re-requested and RPCs
// being to succeed.
func (s) TestErrorFromParentLB_ResourceNotFound(t *testing.T) {
// Start an xDS management server that uses a couple of channels to
// notify the test about the following events:
// - an EDS requested with the expected resource name is requested
// - EDS resource is unrequested, i.e, an EDS request with no resource name
// is received, which indicates that we are no longer interested in that
// resource.
edsResourceRequestedCh := make(chan struct{}, 1)
edsResourceCanceledCh := make(chan struct{}, 1)
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
if req.GetTypeUrl() == version.V3EndpointsURL {
switch len(req.GetResourceNames()) {
case 0:
select {
case edsResourceCanceledCh <- struct{}{}:
default:
}
case 1:
if req.GetResourceNames()[0] == edsServiceName {
select {
case edsResourceRequestedCh <- struct{}{}:
default:
}
}
default:
t.Errorf("Unexpected number of resources, %d, in an EDS request", len(req.GetResourceNames()))
}
}
return nil
},
})
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
server := stubserver.StartTestService(t, nil)
defer server.Stop()
// Configure cluster and endpoints resources in the management server.
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)},
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone)},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Create xDS client, xdsResolver, and dial the test backends.
cc, cleanup := setupAndDial(t, bootstrapContents)
defer cleanup()
// Wait for the EDS resource to be requested.
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for EDS resource to be requested")
case <-edsResourceRequestedCh:
}
// Ensure that a successful RPC can be made.
client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
t.Fatalf("EmptyCall() failed: %v", err)
}
oldCluster := resources.Clusters
// Delete the cluster resource from the management server.
resources.Clusters = nil
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Wait for the EDS resource to be not requested anymore.
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for EDS resource to not be requested")
case <-edsResourceCanceledCh:
}
// Ensure that RPCs start to fail with expected error.
wantErr := fmt.Sprintf("resource %q of type %q has been removed", clusterName, "ClusterResource")
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
_, err := client.EmptyCall(sCtx, &testpb.Empty{})
if status.Code(err) == codes.Unavailable && strings.Contains(err.Error(), wantErr) {
break
}
if err != nil {
t.Logf("EmptyCall failed: %v", err)
}
}
if ctx.Err() != nil {
t.Fatalf("RPCs did not fail after removal of Cluster resource")
}
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
// Add the cluster resource back to the management server.
resources.Clusters = oldCluster
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Wait for the EDS resource to be requested again.
select {
case <-ctx.Done():
t.Fatal("Timeout when waiting for EDS resource to be requested")
case <-edsResourceRequestedCh:
}
// Ensure that a successful RPC can be made.
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); err != nil {
t.Logf("EmptyCall failed: %v", err)
continue
}
break
}
if ctx.Err() != nil {
t.Fatalf("RPCs did not fail after removal of Cluster resource")
}
}
// Test verifies that when the received Cluster resource contains outlier
// detection configuration, the LB config pushed to the priority policy contains
// the appropriate configuration for the outlier detection LB policy.
func (s) TestOutlierDetectionConfigPropagationToChildPolicy(t *testing.T) {
// Unregister the priority balancer builder for the duration of this test,
// and register a policy under the same name that makes the LB config
// pushed to it available to the test.
priorityBuilder := balancer.Get(priority.Name)
internal.BalancerUnregister(priorityBuilder.Name())
lbCfgCh := make(chan serviceconfig.LoadBalancingConfig, 1)
stub.Register(priority.Name, stub.BalancerFuncs{
Init: func(bd *stub.BalancerData) {
bd.ChildBalancer = priorityBuilder.Build(bd.ClientConn, bd.BuildOptions)
},
ParseConfig: func(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
return priorityBuilder.(balancer.ConfigParser).ParseConfig(lbCfg)
},
UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
select {
case lbCfgCh <- ccs.BalancerConfig:
default:
}
return bd.ChildBalancer.UpdateClientConnState(ccs)
},
Close: func(bd *stub.BalancerData) {
bd.ChildBalancer.Close()
},
})
defer balancer.Register(priorityBuilder)
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
server := stubserver.StartTestService(t, nil)
defer server.Stop()
// Configure cluster and endpoints resources in the management server.
cluster := e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone)
cluster.OutlierDetection = &v3clusterpb.OutlierDetection{
Interval: durationpb.New(10 * time.Second),
BaseEjectionTime: durationpb.New(30 * time.Second),
MaxEjectionTime: durationpb.New(300 * time.Second),
MaxEjectionPercent: wrapperspb.UInt32(10),
SuccessRateStdevFactor: wrapperspb.UInt32(2000),
EnforcingSuccessRate: wrapperspb.UInt32(50),
SuccessRateMinimumHosts: wrapperspb.UInt32(10),
SuccessRateRequestVolume: wrapperspb.UInt32(50),
}
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)},
Clusters: []*v3clusterpb.Cluster{cluster},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
SkipValidation: true,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
// Create xDS client, xdsResolver, and dial the test backends.
_, cleanup := setupAndDial(t, bootstrapContents)
defer cleanup()
// The priority configuration generated should have Outlier Detection as a
// direct child due to Outlier Detection being turned on.
wantCfg := &priority.LBConfig{
Children: map[string]*priority.Child{
"priority-0-0": {
Config: &iserviceconfig.BalancerConfig{
Name: outlierdetection.Name,
Config: &outlierdetection.LBConfig{
Interval: iserviceconfig.Duration(10 * time.Second), // default interval
BaseEjectionTime: iserviceconfig.Duration(30 * time.Second),
MaxEjectionTime: iserviceconfig.Duration(300 * time.Second),
MaxEjectionPercent: 10,
SuccessRateEjection: &outlierdetection.SuccessRateEjection{
StdevFactor: 2000,
EnforcementPercentage: 50,
MinimumHosts: 10,
RequestVolume: 50,
},
ChildPolicy: &iserviceconfig.BalancerConfig{
Name: clusterimpl.Name,
Config: &clusterimpl.LBConfig{
Cluster: clusterName,
ChildPolicy: &iserviceconfig.BalancerConfig{
Name: wrrlocality.Name,
Config: &wrrlocality.LBConfig{
ChildPolicy: &iserviceconfig.BalancerConfig{
Name: roundrobin.Name,
},
},
},
},
},
},
},
IgnoreReresolutionRequests: true,
},
},
Priorities: []string{"priority-0-0"},
}
select {
case lbCfg := <-lbCfgCh:
gotCfg := lbCfg.(*priority.LBConfig)
if diff := cmp.Diff(wantCfg, gotCfg); diff != "" {
t.Fatalf("Child policy received unexpected diff in config (-want +got):\n%s", diff)
}
case <-ctx.Done():
t.Fatalf("Timeout when waiting for child policy to receive its configuration")
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package e2e_test
import (
"context"
"fmt"
"testing"
"github.com/google/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/stubserver"
"google.golang.org/grpc/internal/testutils/xds/e2e"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/resolver/manual"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
testpb "google.golang.org/grpc/interop/grpc_testing"
_ "google.golang.org/grpc/internal/xds/balancer/priority" // Register priority LB policy.
)
// TestLogicalDNS_MultipleEndpoints tests the priority LB policy using a
// LOGICAL_DNS discovery mechanism.
//
// The test verifies that multiple addresses returned by the DNS resolver are
// grouped into a single endpoint (as per gRFC A61). Because the round_robin LB
// policy sees only one endpoint, it should not rotate traffic between the
// addresses. Instead, the single endpoint is picked, and connects to the first
// address.
func (s) TestLogicalDNS_MultipleEndpoints(t *testing.T) {
// Spin up a management server to receive xDS resources from.
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
resolverBuilder, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
if err != nil {
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
}
// Start backend servers which provide an implementation of the TestService.
server1 := stubserver.StartTestService(t, nil)
defer server1.Stop()
server2 := stubserver.StartTestService(t, nil)
defer server2.Stop()
// Register a manual resolver with the "dns" scheme to override DNS resolution.
// This global override is safe because connection to the xDS management
// server uses the passthrough scheme instead and therefore overriding
// the DNS resolver does not affect it in any way.
const dnsScheme = "dns"
dnsR := manual.NewBuilderWithScheme(dnsScheme)
originalDNS := resolver.Get("dns")
resolver.Register(dnsR)
t.Cleanup(func() { resolver.Register(originalDNS) })
// For LOGICAL_DNS, this updates the SINGLE endpoint to have 2 IPs.
dnsR.InitialState(resolver.State{
Endpoints: []resolver.Endpoint{{
Addresses: []resolver.Address{
{Addr: server1.Address},
{Addr: server2.Address},
}}},
})
const (
serviceName = "test-xds-service"
clusterName = "cluster-test-xds-service"
endpointsName = "endpoints-test-xds-service"
rdsName = "route-test-xds-service"
)
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, rdsName)},
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, serviceName, clusterName)},
Clusters: []*v3clusterpb.Cluster{e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
ClusterName: clusterName,
ServiceName: endpointsName,
Type: e2e.ClusterTypeLogicalDNS,
DNSHostName: "dns",
DNSPort: uint32(8080),
Policy: e2e.LoadBalancingPolicyRoundRobin,
})},
Endpoints: nil,
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatalf("Failed to update management server: %v", err)
}
// Create a ClientConn and make a successful RPC.
cc, err := grpc.NewClient(fmt.Sprintf("xds:///"+serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolverBuilder))
if err != nil {
t.Fatalf("Failed to create new client for local test server: %v", err)
}
defer cc.Close()
// Ensure the RPC is routed to the first backend.
testClient := testgrpc.NewTestServiceClient(cc)
for i := 0; i < 10; i++ {
var peer peer.Peer
if _, err := testClient.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
t.Fatalf("RPC failed: %v", err)
}
if got, want := peer.Addr.String(), server1.Address; got != want {
t.Errorf("peer.Addr = %q, want = %q", got, want)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package cdsbalancer
import (
"fmt"
"google.golang.org/grpc/grpclog"
internalgrpclog "google.golang.org/grpc/internal/grpclog"
)
const prefix = "[cds-lb %p] "
var logger = grpclog.Component("xds")
func prefixLogger(p *cdsBalancer) *internalgrpclog.PrefixLogger {
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
}